• Stars
    star
    212
  • Rank 186,122 (Top 4 %)
  • Language
    C
  • License
    MIT License
  • Created almost 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

[project suspended] MP4 + H264 encoding for the browser with WASM

πŸ’€ ⚠️

Project Suspended

It has come to my attention that MPEG LA often goes after any distributed software (free / OSS / commercial) that encodes H264 video, and charges huge royalties on it. I don't wish to navigate this legal landscape for this module, so all of the binary and source code surrounding the H264 encoding has now been removed from this project, and the development of the project is suspended until further notice.

However:

  • The documentation and examples here will stay as they provide valuable information for speeding up WASM and media handling (not related to H264)

  • Chrome is still trialing WebCodecs which already supports encoding H264 without incurring royalties (as it is handled by Chrome's licenses). One part of this project was a WASM-based MP4 muxer (not encumbered by MPEG LA patents), and that will be split off into a new WASM module that only muxes already-encoded H264 data. So, at least in future versions of Chrome, fast client-side media creation should still be possible.

(Update: See mp4-wasm for a possible solution that does not depend on an H264 encoder.)

What follows is the previous version of the repository, with all the H264 WASM/C/C++ source code and binary files removed.


mp4-h264

Standalone H264 encoder and MP4 muxer compiled with Emscripten into WASM.

✨ Live Demo (redacted)

Current Features:

  • Encode RGB or YUV data into H264 contained in a MP4 file
  • Fully client-side MP4/H264 encoding, works in most modern browsers
  • Also works in Node.js
  • Memory efficient: can be used for very large video files
  • Relatively small footprint (~144 - 160KB before gzip)
  • Very fast encoding with optional SIMD support
  • Can be used solely as a MP4 muxer, such as alongside the upcoming WebCodecs API for much faster encoding

Possible Future Features:

  • Audio muxing
  • MP4 demuxing
  • ASM.js support for legacy browsers

Contents

Example in a Web Browser

You can import the module directly as an ES Module using <script type="module"> in your HTML. You can use unpkg for easy access, like so:

// Import latest
import loadEncoder from "[redacted]";

(async () => {
  // Load the WASM module first
  const Encoder = await loadEncoder();

  // Create a new encoder interface
  const encoder = Encoder.create({
    width: 256,
    height: 256,
    fps: 30
  });

  // Encode each frame, typically in an animation loop
  for (... each frame ...) {
    const rgba = /* get some Uint8Array of flat RGBA data */;
    encoder.encodeRGB(rgba);
  }

  // Finish encoder to get back uint8 MP4 data
  const mp4 = encoder.end();
  
  // Optionally convert to URL for <video> tag, download, etc...
  const url = URL.createObjectURL(new Blob([mp4], { type: "video/mp4" }));
  ...
})();

See ./test/simple.html for a working example, or a live demo on CodePen.

If you want to host your own files, copy the following from the build directory: mp4-encoder.js, mp4-encoder.wasm, and optionally mp4-encoder.simd.wasm (if you plan to use it). Save them in a folder (e.g. vendor), then you can import the JavaScript directly:

import loadEncoder from './vendor/mp4-encoder.js`;

Example with SIMD (Browser Only)

Ideally you should use the SIMD version for faster encoding, if the browser supports it (only Chrome at the time of writing, and only via a flag). In Chrome, go to about:flags and turn on #enable-webassembly-simd, then reboot.

In code, you need to detect SIMD β€” if you try to load SIMD WASM in an unsupported browser you might run into crashes or errors. You can use wasm-feature-detect for this.

import { simd } from "https://unpkg.com/wasm-feature-detect?module";
import loadEncoder from "[redacted]";

(async () => {
  // Detect SIMD
  const isSIMD = await simd();

  // Load the WASM module
  const Encoder = await loadEncoder({
    simd: isSIMD
  });
})();

Example with Node.js

The default main export of this module is a Node.js compatible WASM build, so this works with newer versions of Node.js. Note that there is no SIMD build for Node.js at the moment.

const loadEncoder = require("[redacted]");

(async () => {
  const Encoder = await loadEncoder();
  // ... same API as web ...
})();

Muxing Only

Let's say you already have H264 data and just want to use this to mux it into an MP4 container, then you'll want to use the lower-level Direct API like so:

const loadEncoder = require('[redacted]');
const fs = require('fs');

(async () => {
  const Encoder = await loadEncoder();
  const stream = fs.createWriteStream('file.mp4');

  const mux = Encoder.create_muxer({
    width,
    height,
    // sequential outputs for simpler 'file write'
    sequential: true,
  }, write);

  for (let chunk of readChunksOfH264FromSomewhere()) {
    // malloc() / free() a pointer
    const p = Encoder.create_buffer(chunk.byteLength);
    // set data in memory
    Encoder.HEAPU8.set(chunk, p);
    // write NAL units with AnnexB format
    // <Uint8Array [startcode] | [NAL] | [startcode] | [NAL] ...>
    Encoder.mux_nal(mux, p, chunk.byteLength);
    Encoder.free_buffer(p);
  }

  // this may trigger more writes
  Encoder.finalize_muxer(mux);

  function write (pointer, size, offset) {
    const buf = Encoder.HEAPU8.slice(pointer, pointer + size);
    stream.write(buf);
    return 0;
  }
})();

See ./test/node-mux.js for a full example.

WebCodecs

If your environment supports WebCodecs, you can use it to achieve much faster encoding (i.e. 3 times faster). This is pretty similar to using "Mux Only", see the ./test/webcodecs.html demo for details.

At the time of writing, WebCodecs is behind a command line flag on Chrome Canary:

  • enable chrome://flags/#enable-experimental-web-platform-features, or
  • pass --enable-blink-features=WebCodecs flag via command line

API Structure

There are two APIs exposed by this module:

  • Simple API
    • A simple wrapper for common usage; this will buffer the output MP4 in memory as it's being written
  • Direct API
    • Direct control over the C/C++ pointers and functions, allowing you to write bytes directly to a file or response stream as they are encoded

Simple API

promise<Encoder> = loadEncoder(config = {})

Loads the WASM encoder and returns the Encoder (web assembly) module. Options:

  • simd (default false) - set to true and the WASM lookup path will be replaced with .simd.wasm, this may lead to crashes/errors in environments that cannot load WASM SIMD modules
  • getWasmPath - an optional function that you can use to override the lookup path, for example if you stored the WASM file in a different place
(async () => {
  const Encoder = await loadEncoder({
    getWasmPath (fileName, scriptDir, isSIMDRequested) {
      return '/my-wasm-files/' + fileName;
    }
  });
})();

encoder = Encoder.create(opt = {})

Creates a new encoder interface with options:

  • width (required) - width in pixels of the video
  • height (required) - height in pixels of the video
  • stride (default 4) - the number of RGB(A) channels used by the encodeRGB() function
  • fps (default 30) - output FPS of the video
  • speed (default 10) - where 0 means best quality and 10 means fastest speed [0..10]
  • kbps (default 0) - the desired bitrate in kilobits per second, if set to 0 then a constant quantizationParameter will be used instead
  • quantizationParameter (default 10) - the constant value to use when kbps is not set, higher means better compression, and lower means better quality [10..51]
  • qpMin (default 10) - when kbps is specified, this is the lower quantization boundary [10..51]
  • qpMax (default 50) - when kbps is specified, this is the upper quantization boundary [10..51]
  • vbvSize (default -1) - set to a negative value to use a default 2 second buffer, or specify 0 to disable Video Buffering Verifier (VBV), or set to a number to specify the size in bytes of the vbv buffer
  • groupOfPictures (default 20) - how often a keyframe occurs (key frame period, also known as GOP)
  • desiredNaluBytes (default 0) - each NAL unit will be approximately capped at this size (0 means unlimited)
  • temporalDenoise (default false) - use temporal noise supression
  • sequential (default false) - set to true if you want MP4 file to be written to sequentially (with no seeking backwards), see here
  • fragmentation (default false) - set to true if you want MP4 file to support HLS streaming playback of the file, see here
  • hevc (default false) - if true, sets the MP4 muxer to expect HEVC (H.265) input instead of H264, this is only useful for muxing your own H265 data

encoder.encodeRGB(pixels)

Encode a frame of RGB(A) pixels, a flat Uint8Array or Uint8ClampedArray. This will use the same stride that you specified when creating the encoder (default 4). Use a stride of 3 if you only have flat RGB bytes, or a stride of 4 if you have RGBA bytes (which is what you get from <canvas> APIs).

This will convert RGB to YUV natively (in WASM) in order to write a single H264 frame of video.

encoder.encodeYUV(yuv)

Encodes an image that is already in YUV layout, saving the need to do the RGB > YUV conversion step.

This may be useful if you already have YUV data, or if you are using a hardware solution to convert RGB to YUV, or if you are multithreading and your renderer thread is idling (in which case you may benefit in memory and performance by converting RGB to YUV in the renderer thread, and then sending that to the encoder thread).

See ./test/util/RGBAtoYUV.js for an example of how to go from RGB to YUV from JavaScript.

uint8 = encoder.end()

Ends the encoding and frees any internal memory used by this encoder interface, returning a final Uint8Array which contains the full MP4 file in bytes. After calling end(), you can no longer use this interface, and instead you'll have to create a new encoder.

uint8 = encoder.memory()

Alias for accessing Encoder.HEAPU8 - note this storage may change as memory is allocated, so you should always access it directly via encoder.memory() rather than storing it as a variable.

ptr = encoder.getRGBPointer() (experimental)

Allocates if necessary, then returns a pointer into the encoder's internal uint8 memory for an RGB(A) buffer (created using stride option).

This might be useful if you have an API that writes pixels directly into an existing buffer with an offset, such as WebGL2's readPixels, to avoid a second data copy.

const ptr = encoder.getRGBPointer();

... render each frame ...
  // read pixels directly into WASM memory
  gl.readPixels(... options ..., encoder.memory(), ptr);
  // trigger an encode from memory
  encoder.encodeRGBPointer();

ptr = encoder.getYUVPointer() (experimental)

Allocates if necessary, then returns a pointer into the encoder's internal uint8 memory that can be used for faster YUV image transfer. See above.

encoder.encodeRGBPointer() (experimental)

Assuming RGB(A) bytes have already been written into the memory heap at the RGB(A) pointer location, it will then encode those bytes directly.

encoder.encodeYUVPointer() (experimental)

Assuming YUV bytes have already been written into the memory heap at the YUV pointer location, it will then encode those bytes directly.

Direct API

Instead of returning an interface with functions, the direct API returns pointers into Encoder.HEAPU8 memory, and then all functions act on the pointer into the encoder's struct.

  • enc = Encoder.create_encoder(settings, write) - allocates and creates an internal struct holding the encoder, the settings are the same as in the Simple API but { stride } is ignored. The write is a write callback that allows you to handle byte writing, with the signature:
    • error = write(data_ptr, data_size, file_seek_offset)
  • ptr = Encoder.create_buffer(byteLength) - creates a pointer to a buffer, for RGB(A) or YUV data, this must be freed manually. Same as ptr = Encoder._malloc(len)
  • Encoder.free_buffer(ptr) - frees a created buffer pointer, same as ptr = Encoder._free(len)
  • Encoder.HEAPU8 - access the current Uint8Array storage of the encoder
  • Encoder.encode_rgb(enc, rgba_ptr, stride, yuv_ptr) - converts RGB into YUV and then encodes it
  • Encoder.encode_yuv(enc, yuv_ptr) - encodes YUV directly
  • Encoder.finalize_encoder(enc) - finishes encoding the MP4 file and frees any memory allocated internally by the encoder structure
  • mux = Encoder.create_muxer(settings, write) - allocates and creates an internal struct holding the muxer (MP4 only), with settings { width, height, [sequential=false, fragmentation=false] } and a write function
  • Encoder.mux_nal(mux, nal_data, nal_size) - writes NAL units to the currently open muxer
  • Encoder.finalize_muxer(mux) - finishes muxing the MP4 file and frees any memory allocated internally
const outputs = [];

const settings = {
  sequential: true,
  width: 256,
  height: 256
};

function write (ptr, size, offset) {
  // take a chunk of memory and push it to stack
  // offset is ignored as we are using { sequential }
  const buf = Encoder.HEAPU8.slice(ptr, size);
  outputs.push(buf);
  return 0; // 0 == success, 1 == error
}

const encoder_ptr = Encoder.create_encoder(settings, write);

const stride = 4; // stride of our RGBA input
const rgb_ptr = Encoder.create_buffer(width * height * stride);
const yuv_ptr = Encoder.create_buffer((width * height * 3) / 2);

for (.. each frame ..) {
  const pixels = /* Uint8Array */;
  Encoder.HEAPU8.set(pixels, rgb_ptr);
  Encoder.encode_rgb(encoder_ptr, rgb_ptr, stride, yuv_ptr);
}

// finish & free memory
Encoder.finalize_encoder(encoder_ptr);
Encoder.free_buffer(rgb_ptr);
Encoder.free_buffer(yuv_ptr);

const mp4File = Buffer.concat(outputs);

Tips for Speed

  • Use WebCodecs where supported
  • Use SIMD if the environment supports it
  • Use a worker to split the rendering and encoding into two threads
  • Use pointers (encoder.encodeRGBPointer()) where possible to avoid unnecessary data copies in and out of WASM memory
  • You can use WebGL to read RGB(A) pixels directly into HEAPU8 memory with gl.readPixels
    • If you are using WebGL1, you can use encoder.memory() or Encoder.HEAPU8 and then subarray() to get a view that you can write into
    • If you are using WebGL2, gl.readPixels includes an offset parameter for where to write into
  • Use OffscreenCanvas if supported and your rendering doesn't need to be visible to the user

Tips for File Size vs. Quality

The default setting produces high quality video, but with very high file sizes (note: the defaults may change as this module is still a WIP).

If you are concerned about filesize, you should set the kbps parameter instead of using a constant quantizationParameter (which defaults to 10, i.e. the best quality).

First, determine what you think would be an acceptable output size based on your video. Let's say you're encoding a 10 second animation and want it to be around 10MB in file size.

const duration = 10; // in seconds
const sizeInMBs = 10; // desired output size
const sizeInKilobits = sizeInMBs * 8000; // conversion

// this is the value passed into the encoder
const kbps = sizeInKilobits / duration;

const encoder = Encoder.create({
  width,
  height,
  fps,
  kbps
})

This should produce a video file around 10 MB, with a quality to match. To improve quality, you can try to lower the { speed } option, or lower the upper bound of { qpMax } from 51 to something smaller (although doing so may increase file size beyond your desired amount).

More Advanced Examples

See the ./test folder for examples including SIMD, multi-threading, fast pixel access, and more.

TODOs

  • Better error handling
  • Explore a more user-friendly and pointer-safe API for writing memory to Emscripten
  • Better support for Webpack, Parcel, esbuild, Rollup etc.
  • Expose more encoder options from C/C++

Contributing

If you think you can help, please open an issue.

Building from C/C++ Source

NOTE: this has now been removed from the project

Currently you need Emscripten with SIMD supported. Then modify the paths in ./src/mp4-encoder/build.sh to your emcc build.Β Then, from this package's folder, run:

./src/mp4-encoder/build.sh 

Credits

This was originally based on h264-mp4-encoder by Trevor Sundberg, but it's been modified quite a bit to reduce the size (~1.7MB to ~150KB), use a different architecture for faster encoding and streamed writing, and use different C libraries (minimp4 instead of libmp4v2).

Also see minimp4 and minih264 by @lieff, which are the lightweight C libraries powering this module.

License

MIT, see LICENSE.md for details.

More Repositories

1

canvas-sketch

[beta] A framework for making generative artwork in JavaScript and the browser.
JavaScript
5,019
star
2

budo

🎬 a dev server for rapid prototyping
JavaScript
2,174
star
3

lwjgl-basics

πŸ”§ LibGDX/LWJGL tutorials and examples
Java
1,841
star
4

graphics-resources

πŸ“ a list of graphic programming resources
1,748
star
5

color-wander

🎨 Generative artwork in node/browser based on a seeded random
JavaScript
1,615
star
6

promise-cookbook

πŸ“™ a brief introduction to using Promises in JavaScript
1,603
star
7

module-best-practices

πŸ“š some best practices for JS modules
JavaScript
1,521
star
8

workshop-generative-art

A workshop on creative coding & generative art
JavaScript
1,362
star
9

svg-mesh-3d

πŸš€ converts a SVG path to a 3D mesh
JavaScript
1,169
star
10

workshop-webgl-glsl

A workshop on WebGL and GLSL
JavaScript
1,032
star
11

webgl-wireframes

Stylized Wireframe Rendering in WebGL
JavaScript
713
star
12

workshop-p5-intro

Intro to Creative Coding workshop with p5.js and Tone.js
711
star
13

canvas-sketch-util

Utilities for sketching in Canvas, WebGL and generative art
JavaScript
661
star
14

threejs-app

Some opinionated structure for a complex/scalable ThreeJS app
JavaScript
444
star
15

bellwoods

JavaScript
395
star
16

webgl-lines

some interactive content for a blog post
JavaScript
385
star
17

eases

a grab-bag of modular easing equations
JavaScript
372
star
18

audiograph.xyz

A visual exploration of Pilotpriest's 2016 album, TRANS.
JavaScript
335
star
19

jsconfeu-generative-visuals

Code for the generative projection mapped animations during JSConf EU 2018 in Berlin.
JavaScript
334
star
20

load-asset

Loads a single or multiple assets and returns a promise.
JavaScript
311
star
21

glsl-fxaa

FXAA implementation for glslify in WebGL
GLSL
310
star
22

dictionary-of-colour-combinations

palettes from A Dictionary of Colour Combinations
Python
290
star
23

shader-reload

An interface for reloading GLSL shaders on the fly.
JavaScript
284
star
24

penplot

[DEPRECATED] see canvas-sketch
JavaScript
262
star
25

mp4-wasm

[proof-of-concept] fast MP4 mux / demux using WASM
C
258
star
26

gifenc

fast GIF encoding
JavaScript
246
star
27

codevember

codevember
JavaScript
242
star
28

impressionist

🎨 generative painting using perlin noise for motion
JavaScript
242
star
29

three-line-2d

lines expanded in a vertex shader
JavaScript
224
star
30

three-orbit-controls

orbit controls for ThreeJS
JavaScript
216
star
31

physical-text

πŸŒ‚ simulating text in the physical world
JavaScript
216
star
32

prot

highly opinionated dev environment [Proof of concept]
JavaScript
201
star
33

template-electron-installation

a template for media art installations using Electron in kiosk mode
JavaScript
199
star
34

workshop-web-audio

Web Audio workshop with Frontend Masters
JavaScript
189
star
35

yyz

JavaScript
187
star
36

parametric-curves

JavaScript
185
star
37

fontpath

Font to vector path tools
JavaScript
183
star
38

ghrepo

:octocat: create a new GitHub repo from your current folder
JavaScript
177
star
39

google-panorama-equirectangular

gets equirectangular images from Google StreetView
JavaScript
172
star
40

image-sdf

generate a signed distance field from an image
JavaScript
171
star
41

glsl-film-grain

natural looking film grain using noise functions
JavaScript
171
star
42

subscapes

generative artwork hosted on Ethereum
JavaScript
169
star
43

pack-spheres

Brute force circle/sphere packing in 2D or 3D
JavaScript
161
star
44

polartone

experimental audio visualizer
JavaScript
154
star
45

dom-css

fast dom CSS styling
JavaScript
153
star
46

tiny-artblocks

Toolkit for small ArtBlocks projects
JavaScript
152
star
47

adaptive-bezier-curve

adaptive and scalable 2D bezier curves
JavaScript
138
star
48

atcq

An implementation of Ant-Tree Color Quantization
JavaScript
136
star
49

workshop-data-artwork

material & notes for a workshop on data artwork & creative coding
JavaScript
125
star
50

kami-demos

🚧 Some demos for the Kami WebGL renderer
JavaScript
122
star
51

rust

experiments
JavaScript
122
star
52

kami

🚧 Rendering ecosystem using Node style packaging
JavaScript
120
star
53

looom-tools

Svelte
115
star
54

esmify

parse and handle import/export for browserify
JavaScript
112
star
55

polyline-normals

gets miter normals for a 2D polyline
JavaScript
112
star
56

three-vignette-background

a simple ThreeJS vignette background
JavaScript
111
star
57

simple-input-events

Unified mouse & touch events for desktop and mobile
JavaScript
105
star
58

tweenr

minimal tweening engine
JavaScript
105
star
59

text-modules

✏️ a list of text/font modules
104
star
60

spectrum

a small tool to visualize the frequencies of an audio file
JavaScript
104
star
61

three-shader-fxaa

optimized FXAA shader for ThreeJS
JavaScript
102
star
62

lerp

bare-bones linear interpolation function
JavaScript
101
star
63

canvas-sketch-cli

A CLI used alongside canvas-sketch
JavaScript
92
star
64

svg-path-contours

gets a discrete list of points from svg
JavaScript
90
star
65

pen-plotter-blog-post

JavaScript
90
star
66

simplify-path

simplify 2D polyline of arrays
JavaScript
83
star
67

garnish

🍸 prettifies ndjson from wzrd and similar tools
JavaScript
81
star
68

get-rgba-palette

gets a palette of prominent colors from an array of pixels
JavaScript
81
star
69

keytime

[EXPERIMENT] keyframe animation tools
JavaScript
79
star
70

three-glslify-example

a simple example of ThreeJS with glslify
GLSL
77
star
71

canvas-text

[experiment] better Canvas2D text rendering
JavaScript
77
star
72

raylight

Experimental WebGL Music Visualizer
JavaScript
76
star
73

verlet-system

2D and 3D verlet integration
JavaScript
75
star
74

word-wrapper

wraps words based on arbitrary 2D glyphs
JavaScript
71
star
75

mp4-wasm-encoder

JavaScript
70
star
76

gl-sprite-text

bitmap font rendering for stackgl
JavaScript
69
star
77

tendril-webtoy-blog-post

A blog post for an interactive Tendril web toy
68
star
78

paper-colors

A small set of pastel and off-white paper colors
JavaScript
68
star
79

threejs-tree-shake

Tree-shakes and optimizes ThreeJS apps
JavaScript
66
star
80

gh-readme-scrape

a CLI to bulk download URLs (images/pdfs/etc) from GitHub readmes
JavaScript
65
star
81

fika

A figma plugin generator
JavaScript
60
star
82

shadertoy-export

render ShaderToy demos to PNG
JavaScript
59
star
83

glsl-random

pseudo-random 2D noise for glslify
C
59
star
84

electron-canvas-to-buffer

in Electron, turns a Canvas into a Buffer
JavaScript
55
star
85

gl-vignette-background

a soft gradient background in WebGL
JavaScript
55
star
86

webpack-three-hmr-test

test of ThreeJS + Webpack + HMR
JavaScript
53
star
87

workshop-generative-color

a workshop on color science for generative art and creative coding
JavaScript
52
star
88

filmic-gl

filmic GLSL shaders in ThreeJS
JavaScript
51
star
89

riso-colors

A list of Risograph printer colors
51
star
90

gsx-pdf-optimize

Optimize PDFs with Ghostscript command
JavaScript
50
star
91

raf-loop

a minimal requestAnimationFrame render loop
JavaScript
49
star
92

gdx-swiper

An example of a "Fruit Ninja" style swipe in LibGDX
Java
47
star
93

gsap-promise

promise wrapper for gsap (TweenLite)
JavaScript
47
star
94

browserify-example

a bare-bones, no-bullshit example of using browserify to dev + build a static demo
JavaScript
45
star
95

extract-svg-path

extracts a string of subpaths from an svg file
JavaScript
45
star
96

figma-plugin-palette

"Image Palette" Plugin in Figma
JavaScript
42
star
97

adaptive-quadratic-curve

adaptive and scalable 2D quadratic curves
JavaScript
42
star
98

three-geometry-data

Get vertex and face data from THREE.Geometry
JavaScript
40
star
99

budo-chrome

an extension of budo dev server that supports live script injection
JavaScript
39
star
100

three-tube-wireframe

Builds a tube-based wireframe geometry in ThreeJS
JavaScript
39
star