JSMpeg Player(TS Player)
- jsmpeg-player has been renamed to @cycjimmy/jsmpeg-player for scoped NPM package.
- JSMpeg player is based on 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
How to use
Install
$ npm install @cycjimmy/jsmpeg-player --save
# or
$ yarn add @cycjimmy/jsmpeg-player
Usage
import JSMpeg from '@cycjimmy/jsmpeg-player'
// new JSMpeg.VideoElement(videoWrapper, videoUrl [, options] [, overlayOptions])
-
JSMpeg.VideoElementconfig: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. adiv); 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 thecanvasoption 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 orhttp(s)://URL to an MPEG-TS resource (the file name does not have to end with.ts), or aws:///wss://URL delivering MPEG-TS. An already createdWebSocketinstance is not accepted. Custom sources (sourceinoverlayOptions) 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. Defaultfalse.autoSetWrapperSize: [Boolean] Whether to set the wrapper element size automatically when the video loaded. Defaultfalse.loop: [Boolean] Whether to loop the video (static files only). Defaultfalse.[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 withplay()/pause(). Defaulttrue.decodeFirstFrame: [Boolean] Whether to decode and display the first frame of the video. Defaulttrue.picMode: [Boolean] Picture mode (no playButton). Defaultfalse.progressive: [Boolean] whether to load data in chunks (static files only). Defaulttrue.chunkSize[Number] The chunk size in bytes to load at a time. Default1024*1024(1mb).hooks: [Object] The hook functionplay: [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 optionsprotocols: [String | Array] WebSocket subprotocols, forwarded tonew WebSocket(url, protocols)(ws://andwss://URLs only). Can carry a Bearer token. Defaultundefined. See WebSocket authentication.preserveDrawingBuffer: [Boolean] Keep the WebGL drawing buffer so the canvas can be read back, e.g. to take a snapshot withcanvas.toDataURL(). Defaultfalse. See Snapshot.
-
JSMpeg.VideoElementinstance supports the following methods:play(): Start playbackpause(): Pause playbackstop(): Stop playback and seek to the beginningdestroy(): Stop playback, release the source, renderer, decoders and audio, and empty video wrapper. Required when the player is removed, see Multiple players.
-
JSMpeg.VideoElement.playerinstance API can view the JSMpeg.Player 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:
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
canvasoption and it already has a WebGL context (becausegetContext('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: truethe player falls back to the Canvas 2D renderer, wheretoDataURL()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):
const video = new JSMpeg.VideoElement('#videoWrapper', 'wss://example.com/stream', {}, {
protocols: ['Bearer.<token>'],
})
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): 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://andwss://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.
// 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) anddestroy()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
<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:
<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.cjsis no longer produced andexportsno longer exposes arequirecondition.require('@cycjimmy/jsmpeg-player')keeps working on Node >= 20.19, because Node canrequire()an ES module, but it returns the module namespace: userequire('@cycjimmy/jsmpeg-player').default.mainandmoduleboth point atdist/jsmpeg-player.esm.js. Thebrowserfield still points at the UMD build for CDN and legacy tooling.engines.nodeis now^22.22.2 || >=24.15.0, the floor of the new build toolchain.enginesis stripped from the published tarball, so it only constrains contributors and CI, not consumers.- The build targets
es2015with thedefaultsbrowserslist, so IE is no longer supported. Babel down-leveling is gone, which makesjsmpeg-player.esm.jsabout 24% smaller andjsmpeg-player.umd.min.jsslightly 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.
// CommonJS consumers: require() can load an ES module from Node 20.19 on
const JSMpeg = require('@cycjimmy/jsmpeg-player').default
Development
$ 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 for the checks the CI runs.
Encoding Video/Audio for jsmpeg by ffmpeg. E.g:
$ 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