# JSMpeg Player(TS Player)

![Test CI][workflows-badge-image]
[![libraries dependency status][libraries-status-image]][libraries-status-url]
[![libraries sourcerank][libraries-sourcerank-image]][libraries-sourcerank-url]
[![Release date][release-date-image]][release-url]
[![vite][vite-image]][vite-url]
[![semantic-release][semantic-image]][semantic-url]
[![npm license][license-image]][download-url]


* **[jsmpeg-player](https://github.com/cycdpo/jsmpeg-player) has been renamed to @cycjimmy/jsmpeg-player for scoped NPM package.**
* JSMpeg player is based on [jsmpeg](https://github.com/phoboslab/jsmpeg).
* The video must be compressed into the TS format of MPEG1 / MP2.
* Autoplay with sound is blocked by the browser until the page receives a user gesture: Apple device automatically plays without sound, you need to guide the user to click on the video in the lower right corner of the video icon to unlock the sound. Chrome and Safari follow the same autoplay policy, where the sound stays in sync with the picture and starts as soon as the user interacts with the page, or can be unlocked right away through the same icon. (no similar problem in non-autoplay mode)
* [Demo][github-pages-url]

## How to use
### Install
[![NPM version][npm-image]][npm-url]
[![NPM bundle size][npm-bundle-size-image]][npm-url]
[![npm download][download-image]][download-url]

```shell
$ npm install @cycjimmy/jsmpeg-player --save
# or
$ yarn add @cycjimmy/jsmpeg-player
```

### Usage
```javascript
import JSMpeg from '@cycjimmy/jsmpeg-player'
```

```javascript
// new JSMpeg.VideoElement(videoWrapper, videoUrl [, options] [, overlayOptions])
```

* `JSMpeg.VideoElement` config:
  * `videoWrapper`: [String | Element] The wrapper of the video — either a **CSS selector that must match an existing element**, or a DOM element itself. It must be a **container element** (e.g. a `div`); do not pass the `<canvas>` itself — the children of a canvas are fallback content and are never rendered, which results in a black screen. Pass the canvas via the `canvas` option instead. The height and width of the wrapper are recommended to be initialized.
  * `videoUrl`: [String] A non-empty raw string. The built-in sources accept a relative or `http(s)://` URL to an MPEG-TS resource (the file name does not have to end with `.ts`), or a `ws://` / `wss://` URL delivering MPEG-TS. An already created `WebSocket` instance is not accepted. Custom sources (`source` in `overlayOptions`) define their own input.
  * `options`: [Object] support:
    * `canvas`: [String | Element] The HTML canvas element to use for video rendering. If none is given, the renderer will create its own canvas element. Default `''`.
    * `poster`: [String] URL to an image to use as the poster to show before the video plays. (Recommended to set it manually)
    * `autoplay`: [Boolean] Whether to start playing immediately. Default `false`.
    * `autoSetWrapperSize`: [Boolean] Whether to set the wrapper element size automatically when the video loaded. Default `false`.
    * `loop`: [Boolean] Whether to loop the video (static files only). Default `false`.**[overwrite]**
    * `control`: [Boolean] Whether the user can control. The built-in play button and click-to-toggle apply to static files only: `ws://` / `wss://` live sources have no built-in playback control and must be driven with `play()` / `pause()`. Default `true`.
    * `decodeFirstFrame`: [Boolean] Whether to decode and display the first frame of the video. Default `true`.
    * `picMode`: [Boolean] Picture mode (no playButton). Default `false`.
    * `progressive`: [Boolean] whether to load data in chunks (static files only). Default `true`.
    * `chunkSize` [Number] The chunk size in bytes to load at a time. Default `1024*1024` (1mb).
    * `hooks`: [Object<Function>] The hook function
      * `play`: [Function] The hook function when the video play.
      * `pause`: [Function] The hook function when the video pause.
      * `stop`: [Function] The hook function when the video stop.
      * `load`: [Function] The hook function when the video established.
  * `overlayOptions`: [Object] More options can view the [jsmpeg options](https://github.com/phoboslab/jsmpeg#usage)
    * `protocols`: [String | Array<String>] WebSocket subprotocols, forwarded to `new WebSocket(url, protocols)` (`ws://` and `wss://` URLs only). Can carry a Bearer token. Default `undefined`. See [WebSocket authentication](#websocket-authentication).
    * `preserveDrawingBuffer`: [Boolean] Keep the WebGL drawing buffer so the canvas can be read back, e.g. to take a snapshot with `canvas.toDataURL()`. Default `false`. See [Snapshot](#snapshot).

* `JSMpeg.VideoElement` instance supports the following methods:
  * `play()`: Start playback
  * `pause()`: Pause playback
  * `stop()`: Stop playback and seek to the beginning
  * `destroy()`: Stop playback, release the source, renderer, decoders and audio, and empty video wrapper. Required when the player is removed, see [Multiple players](#multiple-players).
* `JSMpeg.VideoElement.player` instance API can view the [JSMpeg.Player API](https://github.com/phoboslab/jsmpeg#jsmpegplayer-api)

### Snapshot
The WebGL drawing buffer is cleared once a frame has been composited, so reading the canvas without preparation returns a black image. Set `preserveDrawingBuffer` in `overlayOptions`:

```javascript
const video = new JSMpeg.VideoElement('#videoWrapper', 'video.ts', {}, {
  preserveDrawingBuffer: true,
})

// Any time after the first frame has been rendered
const dataUrl = video.els.canvas.toDataURL('image/png')
```

* The flag is read when the WebGL context is created, so it must be passed at construction time — it can not be enabled on a player that is already running.
* If you pass your own element through the `canvas` option and it already has a WebGL context (because `getContext('webgl')` was called on it before the player was created), that context is reused and the flag is ignored. Let the player create the context instead.
* Keeping the buffer has a performance cost, so leave the flag off if you do not need snapshots.
* WebGL is not required: with `disableGl: true` the player falls back to the Canvas 2D renderer, where `toDataURL()` always works, at the price of a higher CPU usage.

### WebSocket authentication
A browser can not set an `Authorization` header on a WebSocket handshake — `new WebSocket(url, protocols)` is all the WebSocket API offers. The closest browser-native option is to send the token as a subprotocol, which arrives on the server as the `Sec-WebSocket-Protocol` request header (kept out of the URL and out of access logs):

```javascript
const video = new JSMpeg.VideoElement('#videoWrapper', 'wss://example.com/stream', {}, {
  protocols: ['Bearer.<token>'],
})
```

```javascript
const player = new JSMpeg.Player('wss://example.com/stream', {
  protocols: ['Bearer.<token>'],
})
```

* The server must accept the subprotocol and **echo it back** in the handshake response (`Sec-WebSocket-Protocol`). If none of the requested subprotocols is echoed, the browser aborts the connection.
* Each value must be a valid HTTP token ([RFC 7230](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6)): no spaces, `+`, `/` or `=`. JWTs (base64url) fit; tokens containing those characters need cookie-based auth instead (the handshake sends cookies for the target origin automatically).

### Multiple players
Each `JSMpeg.VideoElement` is a complete, independent player: it opens its own source connection, creates its own WebGL context and runs its own decode loop. Only the `AudioContext` and the WASM module are shared, so how many players can play at once is bounded by the browser and the device, not by this library.

* Browsers cap the number of live WebGL contexts per page (16 in Chrome, fewer on some mobile browsers). Past that limit the oldest contexts are dropped and their canvas stays black, without any exception being thrown.
* Every stream is decoded in software on the main thread, and every player allocates its own decoder buffers (512 KB of video, 128 KB of audio by default). A grid of many streams competes for the same CPU time, whatever renderer is used.
* `progressive: true` (the default) keeps one open request per player for the whole stream, which counts against the ~6 connections per origin of HTTP/1.1. This only applies to static files; `ws://` and `wss://` sources are not affected.

**Call `destroy()` when a player goes away.** In a SPA, discarding the instance returned by the constructor — e.g. calling it in `componentDidMount` without storing the result — leaves the player running after its markup is gone: the socket, the WebGL context and the decode loop all survive. A few navigations are then enough to exhaust the page, and since nothing throws, a hard reload appears to be the only fix.

```text
// React
componentDidMount() {
  this.video = new JSMpeg.VideoElement('#videoWrapper', 'wss://example.com/stream');
}

componentWillUnmount() {
  this.video.destroy();
  this.video = null;
}
```

To keep many cameras on screen:

* `disableGl: true` — the Canvas 2D renderer does not consume a WebGL context, at the price of a higher CPU usage.
* `audio: false` — skip audio decoding for camera grids.
* Create players only for the tiles that are visible (e.g. with `IntersectionObserver`) and `destroy()` them when they leave the viewport.
* Serve a low-bitrate sub-stream to the thumbnails; for software decoding the resolution and bitrate of each stream matter more than the number of tiles.

### Use in browser
[![jsdelivr][jsdelivr-image]][jsdelivr-url]

```html
<div id="videoWrapper"></div>
<script src="jsmpeg-player.umd.min.js"></script>
<script>
  var videoUrl = '../static/media/test_video.ts';
  new JSMpeg.VideoElement('#videoWrapper', videoUrl);
</script>
```

## CDN
To use via a CDN include this in your HTML:
```text
<script src="https://cdn.jsdelivr.net/npm/@cycjimmy/jsmpeg-player@7/dist/jsmpeg-player.umd.min.js"></script>
```

## Migrating to v7

v7 drops the CommonJS build and raises the Node and browser targets. `import` and the browser global are unchanged, so most consumers need no change at all.

* `dist/jsmpeg-player.cjs` is no longer produced and `exports` no longer exposes a `require` condition. `require('@cycjimmy/jsmpeg-player')` keeps working on Node >= 20.19, because Node can `require()` an ES module, but it returns the module namespace: use `require('@cycjimmy/jsmpeg-player').default`.
* `main` and `module` both point at `dist/jsmpeg-player.esm.js`. The `browser` field still points at the UMD build for CDN and legacy tooling.
* `engines.node` is now `^22.22.2 || >=24.15.0`, the floor of the new build toolchain. `engines` is stripped from the published tarball, so it only constrains contributors and CI, not consumers.
* The build targets `es2015` with the `defaults` browserslist, so IE is no longer supported. Babel down-leveling is gone, which makes `jsmpeg-player.esm.js` about 24% smaller and `jsmpeg-player.umd.min.js` slightly smaller than v6.
* Deep paths keep working through the `./dist/*` export.
* The runtime CSS is still injected as a `<style>` element; there is no stylesheet to import.

```javascript
// CommonJS consumers: require() can load an ES module from Node 20.19 on
const JSMpeg = require('@cycjimmy/jsmpeg-player').default
```

## Development

```shell
$ npm run dev          # Vite dev server on top of the root index.html
$ npm run lint         # ESLint (flat config, @antfu/eslint-config)
$ npm run lint:style   # Stylelint for src/**/*.{css,scss}
$ npm test             # node --test
$ npm run package      # build dist/ and assemble .release/
```

`index.html` in the repository root is the development entry: it imports `src/` through Vite, so changes show up without a build. `static/index.html` is the GitHub Pages demo and loads the pre-built `jsmpeg-player.umd.js` with a plain `<script>` tag; `npm run build:deployment` assembles it in `.publish/`. See [CONTRIBUTING.md](CONTRIBUTING.md) for the checks the CI runs.

## Encoding Video/Audio for [jsmpeg](https://github.com/phoboslab/jsmpeg) by [ffmpeg](https://ffmpeg.org/). E.g:
```shell
$ ffmpeg -i input.mp4 -f mpegts \
         -codec:v mpeg1video -s 640x360 -b:v 700k -r 25 -bf 0 \
         -codec:a mp2 -ar 44100 -ac 1 -b:a 64k \
         output.ts
```

* options
  * `-s`: video size
  * `-b:v`: video bit rate
  * `-r`: frame rate
  * `-ar`: sampling rate
  * `-ac`: number of audio channels
  * `-b:a`: audio bit rate


<!-- Links: -->
[npm-image]: https://img.shields.io/npm/v/@cycjimmy/jsmpeg-player
[npm-url]: https://npmjs.org/package/@cycjimmy/jsmpeg-player
[npm-bundle-size-image]: https://img.shields.io/bundlephobia/min/@cycjimmy/jsmpeg-player

[download-image]: https://img.shields.io/npm/dt/@cycjimmy/jsmpeg-player
[download-url]: https://npmjs.org/package/@cycjimmy/jsmpeg-player

[jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hy/@cycjimmy/jsmpeg-player
[jsdelivr-url]: https://www.jsdelivr.com/package/npm/@cycjimmy/jsmpeg-player

[workflows-badge-image]: https://github.com/cycjimmy/jsmpeg-player/workflows/Test%20CI/badge.svg

[libraries-status-image]: https://img.shields.io/librariesio/release/npm/@cycjimmy/jsmpeg-player
[libraries-sourcerank-image]: https://img.shields.io/librariesio/sourcerank/npm/@cycjimmy/jsmpeg-player
[libraries-status-url]: https://libraries.io/github/cycjimmy/jsmpeg-player
[libraries-sourcerank-url]: https://libraries.io/npm/@cycjimmy%2Fjsmpeg-player

[release-date-image]: https://img.shields.io/github/release-date/cycjimmy/jsmpeg-player
[release-url]: https://github.com/cycjimmy/jsmpeg-player/releases

[vite-image]: https://img.shields.io/github/package-json/dependency-version/cycjimmy/jsmpeg-player/dev/vite
[vite-url]: https://github.com/vitejs/vite

[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[semantic-url]: https://github.com/semantic-release/semantic-release

[license-image]: https://img.shields.io/npm/l/@cycjimmy/jsmpeg-player

[github-pages-url]: https://cycjimmy.github.io/jsmpeg-player/
