{"id":22297997,"url":"https://github.com/vanilagy/webm-muxer","last_synced_at":"2025-04-10T06:19:04.220Z","repository":{"id":62999276,"uuid":"554946283","full_name":"Vanilagy/webm-muxer","owner":"Vanilagy","description":"WebM multiplexer in pure TypeScript with support for WebCodecs API, video \u0026 audio.","archived":false,"fork":false,"pushed_at":"2025-03-27T20:46:45.000Z","size":2715,"stargazers_count":286,"open_issues_count":2,"forks_count":18,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-03T04:08:45.364Z","etag":null,"topics":["audio","javascript","muxer","typescript","video","webcodecs","webm"],"latest_commit_sha":null,"homepage":"https://vanilagy.github.io/webm-muxer/demo","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Vanilagy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":"vanilagy","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2022-10-20T17:17:28.000Z","updated_at":"2025-03-27T20:45:58.000Z","dependencies_parsed_at":"2023-02-12T15:45:37.716Z","dependency_job_id":"6bc1d18b-5faf-4135-84d2-3f8295f7a428","html_url":"https://github.com/Vanilagy/webm-muxer","commit_stats":{"total_commits":65,"total_committers":1,"mean_commits":65.0,"dds":0.0,"last_synced_commit":"3a7653db1d990b26cac074ce1dc6b990ef3a838c"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vanilagy%2Fwebm-muxer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vanilagy%2Fwebm-muxer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vanilagy%2Fwebm-muxer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vanilagy%2Fwebm-muxer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Vanilagy","download_url":"https://codeload.github.com/Vanilagy/webm-muxer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248166864,"owners_count":21058481,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","javascript","muxer","typescript","video","webcodecs","webm"],"created_at":"2024-12-03T17:52:19.010Z","updated_at":"2025-04-10T06:19:04.197Z","avatar_url":"https://github.com/Vanilagy.png","language":"TypeScript","funding_links":["https://ko-fi.com/vanilagy"],"categories":[],"sub_categories":[],"readme":"# webm-muxer - JavaScript WebM multiplexer\n\n[![](https://img.shields.io/npm/v/webm-muxer)](https://www.npmjs.com/package/webm-muxer)\n[![](https://img.shields.io/bundlephobia/minzip/webm-muxer)](https://bundlephobia.com/package/webm-muxer)\n[![](https://img.shields.io/npm/dm/webm-muxer)](https://www.npmjs.com/package/webm-muxer)\n\nThe WebCodecs API provides low-level access to media codecs, but provides no way of actually packaging (multiplexing)\nthe encoded media into a playable file. This project implements a WebM/Matroska multiplexer in pure TypeScript, which is\nhigh-quality, fast and tiny, and supports video, audio and subtitles as well as live-streaming.\n\n[Demo: Muxing into a file](https://vanilagy.github.io/webm-muxer/demo/)\n\n[Demo: Streaming](https://vanilagy.github.io/webm-muxer/demo-streaming/)\n\n\u003e **Note:** If you're looking to create **MP4** files, check out [mp4-muxer](https://github.com/Vanilagy/mp4-muxer), the\nsister library to webm-muxer.\n\n## Quick start\nThe following is an example for a common usage of this library:\n```js\nimport { Muxer, ArrayBufferTarget } from 'webm-muxer';\n\nlet muxer = new Muxer({\n    target: new ArrayBufferTarget(),\n    video: {\n        codec: 'V_VP9',\n        width: 1280,\n        height: 720\n    }\n});\n\nlet videoEncoder = new VideoEncoder({\n    output: (chunk, meta) =\u003e muxer.addVideoChunk(chunk, meta),\n    error: e =\u003e console.error(e)\n});\nvideoEncoder.configure({\n    codec: 'vp09.00.10.08',\n    width: 1280,\n    height: 720,\n    bitrate: 1e6\n});\n\n/* Encode some frames... */\n\nawait videoEncoder.flush();\nmuxer.finalize();\n\nlet { buffer } = muxer.target; // Buffer contains final WebM file\n```\n\n## Motivation\nThis library was created to power the in-game video renderer of the browser game\n[Marble Blast Web](https://github.com/vanilagy/marbleblast) - [here](https://www.youtube.com/watch?v=ByCcAIoXsKY) you\ncan find a video completely rendered by it and muxed with this library. Previous efforts at in-browser WebM muxing,\nsuch as [webm-writer-js](https://github.com/thenickdude/webm-writer-js) or\n[webm-muxer.js](https://github.com/davedoesdev/webm-muxer.js), were either lacking in functionality or were way too\nheavy in terms of byte size, which prompted the creation of this library.\n\n## Installation\nUsing NPM, simply install this package using\n```\nnpm install webm-muxer\n```\nYou can import all exported classes like so:\n```js\nimport * as WebMMuxer from 'webm-muxer';\n// Or, using CommonJS:\nconst WebMMuxer = require('webm-muxer');\n```\nAlternatively, you can simply include the library as a script in your HTML, which will add a `WebMMuxer` object,\ncontaining all the exported classes, to the global object, like so:\n```html\n\u003cscript src=\"build/webm-muxer.js\"\u003e\u003c/script\u003e\n```\n\n## Usage\n### Initialization\nFor each WebM file you wish to create, create an instance of `Muxer` like so:\n```js\nimport { Muxer } from 'webm-muxer';\n\nlet muxer = new Muxer(options);\n```\nThe available options are defined by the following interface:\n```ts\ninterface MuxerOptions {\n    target:\n        | ArrayBufferTarget\n        | StreamTarget\n        | FileSystemWritableFileStreamTarget,\n\n    video?: {\n        codec: string,\n        width: number,\n        height: number,\n        frameRate?: number, // Optional, adds metadata to the file\n        alpha?: boolean // If the video contains transparency data\n    },\n\n    audio?: {\n        codec: string,\n        numberOfChannels: number,\n        sampleRate: number,\n        bitDepth?: number // Mainly necessary for PCM-coded audio\n    },\n\n    subtitles?: {\n        codec: string\n    },\n\n    streaming?: boolean,\n\n    type?: 'webm' | 'matroska',\n\n    firstTimestampBehavior?: 'strict' | 'offset' | 'permissive'\n}\n```\nCodecs officially supported by WebM are:\\\n**Video:** `V_VP8`, `V_VP9`, `V_AV1`\\\n**Audio:** `A_OPUS`, `A_VORBIS`\\\n**Subtitles:** `S_TEXT/WEBVTT`\n#### `target`\nThis option specifies where the data created by the muxer will be written. The options are:\n- `ArrayBufferTarget`: The file data will be written into a single large buffer, which is then stored in the target.\n\n    ```js\n    import { Muxer, ArrayBufferTarget } from 'webm-muxer';\n\n    let muxer = new Muxer({\n        target: new ArrayBufferTarget(),\n        // ...\n    });\n\n    // ...\n\n    muxer.finalize();\n    let { buffer } = muxer.target;\n    ```\n- `StreamTarget`: This target defines callbacks that will get called whenever there is new data available  - this is\n    useful if you want to stream the data, e.g. pipe it somewhere else. The constructor has the following signature:\n\n    ```ts\n    constructor(options: {\n        onData?: (data: Uint8Array, position: number) =\u003e void,\n        onHeader?: (data: Uint8Array, position: number) =\u003e void,\n        onCluster?: (data: Uint8Array, position: number, timestamp: number) =\u003e void,\n        chunked?: boolean,\n        chunkSize?: number\n    });\n    ```\n\n    `onData` is called for each new chunk of available data. The `position` argument specifies the offset in bytes at\n    which the data has to be written. Since the data written by the muxer is not entirely sequential, **make sure to\n    respect this argument**.\n    \n    When using `chunked: true`, data created by the muxer will first be accumulated and only written out once it has\n    reached sufficient size. This is useful for reducing the total amount of writes, at the cost of latency. It using a\n    default chunk size of 16 MiB, which can be overridden by manually setting `chunkSize` to the desired byte length.\n    \n    If you want to use this target for *live-streaming*, make sure to also set `streaming: true` in the muxer options.\n    This will ensure that data is written monotonically (sequentially) and already-written data is never \"patched\" -\n    necessary for live-streaming, but not recommended for muxing files for later viewing.\n\n    The `onHeader` and `onCluster` callbacks will be called for the file header and each Matroska cluster, respectively.\n    This way, you don't need to parse them out yourself from the data provided by `onData`.\n\n    ```js\n    import { Muxer, StreamTarget } from 'webm-muxer';\n\n    let muxer = new Muxer({\n        target: new StreamTarget({\n            onData: (data, position) =\u003e { /* Do something with the data */ }\n        }),\n        // ...\n    });\n    ```\n- `FileSystemWritableFileStreamTarget`: This is essentially a wrapper around a chunked `StreamTarget` with the intention\n    of simplifying the use of this library with the File System Access API. Writing the file directly to disk as it's\n    being created comes with many benefits, such as creating files way larger than the available RAM.\n\n    You can optionally override the default `chunkSize` of 16 MiB.\n    ```ts\n    constructor(\n        stream: FileSystemWritableFileStream,\n        options?: { chunkSize?: number }\n    );\n    ```\n\n    Usage example:\n    ```js\n    import { Muxer, FileSystemWritableFileStreamTarget } from 'webm-muxer';\n    \n    let fileHandle = await window.showSaveFilePicker({\n        suggestedName: `video.webm`,\n        types: [{\n            description: 'Video File',\n            accept: { 'video/webm': ['.webm'] }\n        }],\n    });\n    let fileStream = await fileHandle.createWritable();\n    let muxer = new Muxer({\n        target: new FileSystemWritableFileStreamTarget(fileStream),\n        // ...\n    });\n    \n    // ...\n\n    muxer.finalize();\n    await fileStream.close(); // Make sure to close the stream\n    ```\n#### `streaming` (optional)\nConfigures the muxer to only write data monotonically, useful for live-streaming the WebM as it's being muxed; intended\nto be used together with the `target` set to type `function`. When enabled, some features such as storing duration and\nseeking will be disabled or impacted, so don't use this option when you want to write out WebM file for later use.\n#### `type` (optional)\nAs WebM is a subset of the more general Matroska multimedia container format, this library muxes both WebM and Matroska\nfiles. WebM, according to the official specification, supports only a small subset of the codecs supported by Matroska.\nIt is likely, however, that most players will successfully play back a WebM file with codecs other than the ones\nsupported in the spec. To be on the safe side, however, you can set the `type` option to `'matroska'`, which\nwill internally label the file as a general Matroska file. If you do this, your output file should also have the .mkv\nextension.\n#### `firstTimestampBehavior` (optional)\nSpecifies how to deal with the first chunk in each track having a non-zero timestamp. In the default strict mode,\ntimestamps must start with 0 to ensure proper playback. However, when directly pumping video frames or audio data\nfrom a MediaTrackStream into the encoder and then the muxer, the timestamps are usually relative to the age of\nthe document or the computer's clock, which is typically not what we want. Handling of these timestamps must be\nset explicitly:\n- Use `'offset'` to offset the timestamp of each video track by that track's first chunk's timestamp. This way, it\nstarts at 0.\n- Use `'permissive'` to allow the first timestamp to be non-zero.\n\n### Muxing media chunks\nThen, with VideoEncoder and AudioEncoder set up, send encoded chunks to the muxer using the following methods:\n```ts\naddVideoChunk(\n    chunk: EncodedVideoChunk,\n    meta?: EncodedVideoChunkMetadata,\n    timestamp?: number\n): void;\n\naddAudioChunk(\n    chunk: EncodedAudioChunk,\n    meta?: EncodedAudioChunkMetadata,\n    timestamp?: number\n): void;\n```\n\nBoth methods accept an optional, third argument `timestamp` (microseconds) which, if specified, overrides\nthe `timestamp` property of the passed-in chunk.\n\nThe metadata comes from the second parameter of the `output` callback given to the\nVideoEncoder or AudioEncoder's constructor and needs to be passed into the muxer, like so:\n```js\nlet videoEncoder = new VideoEncoder({\n    output: (chunk, meta) =\u003e muxer.addVideoChunk(chunk, meta),\n    error: e =\u003e console.error(e)\n});\nvideoEncoder.configure(/* ... */);\n```\n\nShould you have obtained your encoded media data from a source other than the WebCodecs API, you can use these following\nmethods to directly send your raw data to the muxer:\n```ts\naddVideoChunkRaw(\n    data: Uint8Array,\n    type: 'key' | 'delta',\n    timestamp: number, // In microseconds\n    meta?: EncodedVideoChunkMetadata\n): void;\n\naddAudioChunkRaw(\n    data: Uint8Array,\n    type: 'key' | 'delta',\n    timestamp: number, // In microseconds\n    meta?: EncodedAudioChunkMetadata\n): void;\n```\n\n### Finishing up\nWhen encoding is finished and all the encoders have been flushed, call `finalize` on the `Muxer` instance to finalize\nthe WebM file:\n```js\nmuxer.finalize();\n```\nWhen using an ArrayBufferTarget, the final buffer will be accessible through it:\n```js\nlet { buffer } = muxer.target;\n```\nWhen using a FileSystemWritableFileStreamTarget, make sure to close the stream after calling `finalize`:\n```js\nawait fileStream.close();\n```\n\n## Details\n### Media chunk buffering\nWhen muxing a file with a video **and** an audio track, it is important that the individual chunks inside the WebM file\nbe stored in monotonically increasing time. This does mean, however, that the multiplexer must buffer chunks of one\nmedium if the other medium has not yet encoded chunks up to that timestamp. For example, should you first encode all\nyour video frames and then encode the audio afterwards, the multiplexer will have to hold all those video frames in\nmemory until the audio chunks start coming in. This might lead to memory exhaustion should your video be very long.\nWhen there is only one media track, this issue does not arise. So, when muxing a multimedia file, make sure it is\nsomewhat limited in size or the chunks are encoded in a somewhat interleaved way (like is the case for live media).\n\n### Subtitles\nThis library supports adding a subtitle track to a file. Like video and audio, subtitles also need to be encoded before\nthey can be added to the muxer. To do this, this library exports its own `SubtitleEncoder` class with a WebCodecs-like\nAPI. Currently, it only supports encoding WebVTT files.\n\nHere's a full example using subtitles:\n```js\nimport { Muxer, SubtitleEncoder, ArrayBufferTarget } from 'webm-muxer';\n\nlet muxer = new Muxer({\n    target: new ArrayBufferTarget(),\n    subtitles: {\n        codec: 'S_TEXT/WEBVTT'\n    },\n    // ....\n});\n\nlet subtitleEncoder = new SubtitleEncoder({\n    output: (chunk, meta) =\u003e muxer.addSubtitleChunk(chunk, meta),\n    error: e =\u003e console.error(e)\n});\nsubtitleEncoder.configure({\n    codec: 'webvtt'\n});\n\nlet simpleWebvttFile =\n`WEBVTT\n\n00:00:00.000 --\u003e 00:00:10.000\nExample entry 1: Hello \u003cb\u003eworld\u003c/b\u003e.\n`;\nsubtitleEncoder.encode(simpleWebvttFile);\n\n// ...\n\nmuxer.finalize();\n```\n\nYou do not need to encode an entire WebVTT file in one go; you can encode individual cues or any number of them at once.\nJust make sure that the preamble (the part before the first cue) is the first thing to be encoded.\n\n### Size \"limits\"\nThis library can mux WebM files up to a total size of ~4398 GB and with a Matroska Cluster size of ~34 GB.\n\n## Implementation \u0026 development\nWebM files are a subset of the more general Matroska media container format. Matroska in turn uses a format known as\nEBML (think of it like binary XML) to structure its file. This project therefore implements a simple EBML writer to\ncreate the Matroska elements needed to form a WebM file. Many thanks to\n[webm-writer-js](https://github.com/thenickdude/webm-writer-js) for being the inspiration for most of the core EBML\nwriting code.\n\nFor development, clone this repository, install everything with `npm install`, then run `npm run watch` to bundle the\ncode into the `build` directory. Run `npm run check` to run the TypeScript type checker, and `npm run lint` to run\nESLint.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvanilagy%2Fwebm-muxer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvanilagy%2Fwebm-muxer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvanilagy%2Fwebm-muxer/lists"}