# jmuxer remuxes raw H264 and AAC into MP4 inside the browser

> jmuxer is a small JavaScript muxer that takes raw H264 or H265 video and AAC audio in ADTS, wraps them into fragmented MP4 and feeds the result to the browser through the Media Source Extensions API, with a Node path that can export an MP4 file instead.

**webstream-labs/jmuxer** — jMuxer - a simple javascript mp4 muxer that works in both browser and node environment.

- Repository: https://github.com/webstream-labs/jmuxer
- Stars: 644 · Forks: 125
- Language: JavaScript
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/webstream-labs-jmuxer

## From elementary stream to playable MP4

jmuxer is described as a simple JavaScript mp4 muxer that works in both browser and Node environment. The README adds the key constraints: it is communication protocol agnostic, it is intended to play media files on the browser with the help of the media source extension, and it can also export mp4 in the node environment. It expects raw H264 video data and/or AAC audio data in ADTS container as an input.

That last sentence defines the whole project. The input is not a media file and not an MP4, it is the elementary stream as it comes off a decoder or a camera, and the job is to wrap it in the boxes a browser will accept. Because the tool is protocol agnostic, it does not care how the bytes arrived: RTSP, WebSocket, HTTP, a WebRTC data channel or a WebRTC MediaStream, as long as the payload is H264 or AAC.

The browser side works through the Media Source Extensions API. A video element is attached to a MediaSource, the muxer appends fragmented MP4 segments to a SourceBuffer, and the browser plays them. The point of the project is that it removes the need for a full player stack, and it is small enough to read.

The repository is a conventional JavaScript library: src for the source, dist for the built bundle, a rollup configuration, Babel for transpilation, an ESLint flat config, an example directory with a demo server, and a docs directory. The package is published on npm as jmuxer, and the package metadata records version 2.1.4.

There is a live demo hosted on GitHub pages, plus a separate page that plays a raw H264 file online, which is the quickest way to see the library work without writing any code.

## Feeding the muxer: the data object and its awkward flags

Media data is handed to the feed method as an object, and the README documents the properties it may carry.

The video property holds the h264 or h265 buffer and the audio property holds the AAC buffer. At least one of them must be present. The duration property is the duration in milliseconds of the provided chunk, and if no duration is given the muxer calculates the frame duration using the provided frame rate.

Two of the properties are the interesting ones for anyone with real camera output. The compositionTimeOffset is the difference between decode time and presentation time of frames in milliseconds, and the README says it is only used for video and is usually needed when B-frames are present in the video stream. B-frames reorder frames, so without this the player can present them in the wrong order.

The isLastVideoFrameComplete flag says whether the last frame in the provided buffer is complete and can immediately be pushed to the remuxer. The README is careful about the terminology here: a frame in this context refers to an H264 or H265 frame, which might or might not correspond to a frame of video. The flag exists to remove a one frame delay that is naturally present because jMuxer does not know whether the data it received is complete unless the next NALU header arrives. The advice is explicit: use it only if you are sure of it, otherwise issues may arise. It defaults to false.

That caveat is the sort of thing that saves hours. A muxer that guesses at frame boundaries will either drop a frame or stall, and the symptom appears far from the cause.

## Options that control buffering, codec and frame rate

The option list is long, and most of it is about timing and about what the muxer is allowed to assume.

The node option is the id of a video tag or a reference to the HTMLVideoElement, and it is a required field for browsers. The mode option takes both, video or audio, and defaults to both. The videoCodec option is either H264 or H265, defaulting to H264, and the README notes that the browser must support H265 for that to work, which in practice means the H265 path fails silently in a browser that cannot decode it unless you watch the unsupported codec callback.

The buffering options are the ones worth thinking about. flushingTime is the buffer flushing time in milliseconds, with a default of 500, and setting it to 0 flushes the buffer immediately or is a way to find any lag. maxDelay is the maximum delay time in milliseconds, also defaulting to 500. Those two together determine how much latency the player adds and how stable the buffer is, and the defaults are a compromise rather than a recommendation for a particular use case.

The clearBuffer option, true by default, decides whether played media buffer is cleared automatically, which is what keeps memory bounded on a long stream. The fps option is optional and holds the frame rate if it is known, used to find the frame duration when the chunk duration is not available. The readFpsFromTrack option, false by default, takes the frame rate from MP4 track data instead.

The debug option, false by default, prints a debug log in the browser console, and the two logger callbacks, onLoggerLog and onLoggerErr, are used when debug is enabled and default to console.log and console.error.

## The callbacks tell you what the browser cannot

The event callbacks are where the library reports the things a player needs to know and cannot discover on its own, and the set is more complete than most.

onReady fires once MSE is ready. onData fires when muxed data is ready to be used, with the muxed data as the first argument, which is the hook the Node export path and any server side muxing build on. onError fires if the muxer encounters any buffer related errors.

Two callbacks exist purely to report the browser's own limits. onUnsupportedCodec fires when trying to play a codec the browser does not support, and onMissingVideoFrames and onMissingAudioFrames fire when the muxer encounters missing frames in the corresponding track. Those are the signals that a source is dropping data rather than merely being slow.

The one that matters most for a player is onKeyframePosition, which fires when a keyframe is detected, and the point of it is that the provided time is seekable. In MSE, seeking is only possible to a point where a keyframe was appended, so a UI that offers a seek bar has to know the valid positions. A library that reports them is saving you the bookkeeping of tracking them from the feed calls yourself.

For a server that muxes on the server side and streams fragmented MP4 to clients, the onData callback plus the feed method is the entire integration: mux once, send the segments, and let each client append them.

## The Node path: streams, callbacks and exporting a file

The Node usage is documented with two approaches, and they suit different situations.

The first uses a writable stream. The muxer exposes createStream, which the README marks as available on NodeJS only, and a feeder stream is piped into it and then into whatever output you have, an HTTP response, a WebSocket or a file:

```js
const JMuxer = require('jmuxer');
const jmuxer = new JMuxer({
    debug: true
});

/*
Stream in Object mode. Please check the example file for more details
*/
let h264_feeder = getFeederStreamSomehow();
let http_or_ws_or_any = getWritterStreamSomehow();
h264_feeder.pipe(jmuxer.createStream()).pipe(http_or_ws_or_any);
```

The second approach uses the onData callback and writes the muxed bytes yourself, which is the pattern to use when the output is not a stream, for example when the segments are being forwarded to a client that is not a plain HTTP response.

The methods table is short, which is a good sign for a library of this size. feed takes the data object described earlier. createStream returns a writable stream to feed the buffer, on NodeJS only. reset resets the muxer so it can start over, which matters for a reconnecting source. destroy destroys the muxer instance and releases the resources, and for a long-lived server that handles many connections, calling it is the difference between a clean shutdown and a leak.

The example directory carries the pieces these snippets refer to. There is toFile.mjs for the export path, server.mjs for the demo server, demo.h264 and demo.h265 as raw samples, chunks for the pre-cut data, and separate HTML pages for H264 with AAC and for H265.

## Two demos, one dev server

The README describes two demos, and the distinction between them is the clearest statement of what the library is for.

In the first, raw chunks come from the server and the client does the muxing, which is the pure client-side model where the browser receives elementary stream data and jmuxer turns it into playable MP4. In the second, the server does the muxing and streams fragmented MP4 to the client, which is the model for a server that has to serve many viewers at once and would rather mux once.

Both are run from the root directory of the repository:

```bash
npm i
npm run dev
```

and then you navigate to the local server:

```
http://localhost:8080/
```

The README points at example/server.mjs for the details of how they work.

The dev command is a two-process setup declared in the package scripts. The dev script runs concurrently with the names BUILD and SERVER, invoking the watch build and the server script in parallel, and the server script itself changes into the example directory and runs nodemon on server.mjs. The build script is rollup, the watch script adds the rollup watch flag, and the production script sets NODE_ENV=production for the rollup run.

The dev dependencies explain the rest of the toolchain. Express 5 and express-ws provide the HTTP and WebSocket server for the demos, msgpack-lite handles the binary framing, nodemon restarts the server on change, concurrently runs the two processes, and the Rollup plugins cover Babel transpilation, environment variable replacement and Terser minification. The dist folder holds the built bundle, jmuxer.min.js, which is the file the HTML usage example loads from a script tag.

## Browser support, packaging and the licence question

The compatibility list is stated in terms of Media Source Extensions with video/MP4, and it is a specific list: Chrome for Android 34+, Chrome for Desktop 34+, Firefox for Android 41+, Firefox for Desktop 42+, IE11+ for Windows 8.1+, Edge for Windows 10+, Opera for Desktop, and Safari for Mac 8+.

The Safari entry is the one to look at twice, since Safari's Media Source Extensions support arrived later than the number suggests and the list was written some time ago. The README also notes that the browser must support H265 for the H265 path to work, which narrows the practical support for that codec considerably.

Packaging is straightforward. The browser path uses a script tag pointing at dist/jmuxer.min.js and constructs a JMuxer with an options object. The ES6 path installs the module with npm install --save jmuxer and imports it. TypeScript users install the separate types package with npm install --save @types/jmuxer, which tells you the type definitions live outside the main package.

The licence is where the record is thin. The repository's licence is recorded as a custom licence that GitHub cannot classify, with a pointer to the LICENSE file, and the README does not restate the terms. That is worth resolving before the code goes into a product, and it is a reading of where to look rather than a legal opinion.

On versions, the published tags are v2.1.2 on 2026-08-26, v2.1.0 on 2025-09-22 and v2.0.7 on 2024-08-25, while the package metadata in the repository reads 2.1.4, so the working tree is ahead of the last tag. The last push was on 2026-10-13 and the repository is not archived.

## Conclusion

jmuxer fits a developer whose transport gives them elementary stream data rather than a container, a camera that emits raw H264 over RTSP, or a server that has to hand a browser something playable without pulling in a heavyweight media stack, and who is content to work at the chunk level rather than through a player library. It is the wrong choice if you already receive fMP4 or HLS, since the remuxing it does is then unnecessary, and it is the wrong choice if you need browser playback on a platform without Media Source Extensions. Before adopting it, confirm the codec profile of your source is H264 or H265 with AAC in ADTS, check the browser support list against the devices you care about, and read the flushingTime and maxDelay defaults, because they trade latency against buffer stability in a way that has to be tuned per stream.

## FAQ

### What input does jmuxer expect?

Raw H264 or H265 video data and/or AAC audio data in ADTS container. The feed method takes an object with audio, video and duration, and at least one of the media properties must be provided.

### How do I use jmuxer in a browser?

Load dist/jmuxer.min.js with a script tag, give the constructor the id of a video element through the node option, and call feed with your media data. The library muxes the data into fragmented MP4 and appends it through the Media Source Extensions API.

### Can jmuxer write an MP4 file in Node?

Yes. The README documents a Node path where createStream returns a writable stream to pipe a feeder into, and an alternative using the onData callback to write muxed bytes yourself. The example directory includes a toFile.mjs file for the export path.

### What does the onKeyframePosition callback do in jmuxer?

It fires when a keyframe is detected, and the time it provides is seekable. That is how a player knows which positions it can offer in a seek bar, since seeking in Media Source Extensions is only possible to a point where a keyframe was appended.

### Which browsers does jmuxer support?

Browsers supporting Media Source Extensions with video/MP4: Chrome for Android and Desktop 34+, Firefox for Android 41+ and Desktop 42+, IE11+ on Windows 8.1+, Edge for Windows 10+, Opera for Desktop, and Safari for Mac 8+. H265 additionally requires browser support for that codec.

## Sources

- [Issues](https://github.com/webstream-labs/jmuxer/issues)
- [README](https://github.com/webstream-labs/jmuxer/blob/master/README.md)
- [Releases](https://github.com/webstream-labs/jmuxer/releases)
- [webstream-labs/jmuxer on GitHub](https://github.com/webstream-labs/jmuxer)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/webstream-labs-jmuxer
