Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/dmnsgn/canvas-record

Record a video in the browser or directly on the File System from a canvas (2D/WebGL/WebGPU) as MP4, WebM, MKV, GIF, PNG/JPG Sequence using WebCodecs and Wasm when available.
https://github.com/dmnsgn/canvas-record

canvas capture download filesystem gif mp4 record video webcodecs

Last synced: 3 months ago
JSON representation

Record a video in the browser or directly on the File System from a canvas (2D/WebGL/WebGPU) as MP4, WebM, MKV, GIF, PNG/JPG Sequence using WebCodecs and Wasm when available.

Awesome Lists containing this project

README

        

# canvas-record

[![npm version](https://img.shields.io/npm/v/canvas-record)](https://www.npmjs.com/package/canvas-record)
[![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://www.npmjs.com/package/canvas-record)
[![npm minzipped size](https://img.shields.io/bundlephobia/minzip/canvas-record)](https://bundlephobia.com/package/canvas-record)
[![dependencies](https://img.shields.io/librariesio/release/npm/canvas-record)](https://github.com/dmnsgn/canvas-record/blob/main/package.json)
[![types](https://img.shields.io/npm/types/canvas-record)](https://github.com/microsoft/TypeScript)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fa6673.svg)](https://conventionalcommits.org)
[![styled with prettier](https://img.shields.io/badge/styled_with-Prettier-f8bc45.svg?logo=prettier)](https://github.com/prettier/prettier)
[![linted with eslint](https://img.shields.io/badge/linted_with-ES_Lint-4B32C3.svg?logo=eslint)](https://github.com/eslint/eslint)
[![license](https://img.shields.io/github/license/dmnsgn/canvas-record)](https://github.com/dmnsgn/canvas-record/blob/main/LICENSE.md)

Record a video in the browser or directly on the File System from a canvas (2D/WebGL/WebGPU) as MP4, WebM, MKV, GIF, PNG/JPG Sequence using WebCodecs and Wasm when available.

[![paypal](https://img.shields.io/badge/donate-paypal-informational?logo=paypal)](https://paypal.me/dmnsgn)
[![coinbase](https://img.shields.io/badge/donate-coinbase-informational?logo=coinbase)](https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3)
[![twitter](https://img.shields.io/twitter/follow/dmnsgn?style=social)](https://twitter.com/dmnsgn)

![](https://raw.githubusercontent.com/dmnsgn/canvas-record/main/screenshot.gif)

## Installation

```bash
npm install canvas-record
```

## Usage

```js
import { Recorder, RecorderStatus, Encoders } from "canvas-record";
import createCanvasContext from "canvas-context";
import { AVC } from "media-codecs";

// Setup
const pixelRatio = devicePixelRatio;
const width = 512;
const height = 512;
const { context, canvas } = createCanvasContext("2d", {
width: width * pixelRatio,
height: height * pixelRatio,
contextAttributes: { willReadFrequently: true },
});
Object.assign(canvas.style, { width: `${width}px`, height: `${height}px` });

const mainElement = document.querySelector("main");
mainElement.appendChild(canvas);

// Animation
let canvasRecorder;

function render() {
const width = canvas.width;
const height = canvas.height;

const t = canvasRecorder.frame / canvasRecorder.frameTotal || Number.EPSILON;

context.clearRect(0, 0, width, height);
context.fillStyle = "red";
context.fillRect(0, 0, t * width, height);
}

const tick = async () => {
render();

if (canvasRecorder.status !== RecorderStatus.Recording) return;
await canvasRecorder.step();

if (canvasRecorder.status !== RecorderStatus.Stopped) {
requestAnimationFrame(() => tick());
}
};

canvasRecorder = new Recorder(context, {
name: "canvas-record-example",
encoderOptions: {
codec: AVC.getCodec({ profile: "Main", level: "5.2" }),
},
});

// Start and encode frame 0
await canvasRecorder.start();

// Animate to encode the rest
tick(canvasRecorder);
```

## API

Encoder comparison:

| Encoder | Extension | Required Web API | WASM | Speed |
| -------------- | ---------------------- | ------------------ | --------------------- | -------- |
| `WebCodecs` | `mp4` / `webm` / `mkv` | WebCodecs | ❌ | Fast |
| `MP4Wasm` | `mp4` | WebCodecs | ✅ (embed) | Fast |
| `H264MP4` | `mp4` | | ✅ (embed) | Medium |
| `FFmpeg` | `mp4` / `webm` | SharedArrayBuffer | ✅ (need binary path) | Slow |
| `GIF` | `gif` | WebWorkers (wip) | ❌ | Fast |
| `Frame` | `png` / `jpg` | File System Access | ❌ | Fast |
| `MediaCapture` | `mkv` / `webm` | MediaStream | ❌ | Realtime |

Note:

- `WebCodecs` encoderOptions allow different codecs to be used: VP8/VP9/AV1/HEVC. See [media-codecs](https://github.com/dmnsgn/media-codecs) to get a codec string from human readable options and check which ones are supported in your browser with [github.io/media-codecs](https://dmnsgn.github.io/media-codecs/).
- `WebCodecs` 5-10x faster than H264MP4Encoder and 20x faster than `FFmpeg` (it needs to mux files after writing png to virtual FS)
- `FFmpeg` (mp4 and webm) and `WebCodecs` (mp4) have a AVC maximum frame size of 9437184 pixels. That's fine until a bit more than 4K 16:9 @ 30fps. So if you need 4K Square or 8K exports, be patient with `H264MP4Encoder` (which probably also has the 4GB memory limit) or use Frame encoder and mux them manually with `FFmpeg` CLI (`ffmpeg -framerate 30 -i "%05d.jpg" -b:v 60M -r 30 -profile:v baseline -pix_fmt yuv420p -movflags +faststart output.mp4`)
- `MP4Wasm` is embedded from [mp4-wasm](https://github.com/mattdesl/mp4-wasm/) for ease of use (`FFmpeg` will require `encoderOptions.corePath`)

Roadmap:

- [ ] add debug logging
- [ ] use WebWorkers for gifenc

## Modules


canvas-record


Re-export Recorder, RecorderStatus, all Encoders and utils.



## Classes


Recorder


Encoder


FFmpegEncoder


FrameEncoder


GIFEncoder


H264MP4Encoder


MediaCaptureEncoder


MP4WasmEncoder


WebCodecsEncoder


## Constants



isWebCodecsSupported : boolean


Check for WebCodecs support on the current platform.



## Functions



estimateBitRate(width, height, frameRate, motionRank, bitrateMode)number


Estimate the bit rate of a video rounded to nearest megabit.
Based on "H.264 for the rest of us" by Kush Amerasinghe.



## Typedefs



onStatusChangeCb : function


A callback to notify on the status change. To compare with RecorderStatus enum values.




RecorderOptions : object


Options for recording. All optional.




RecorderStartOptions : object


Options for recording initialisation. All optional.




EncoderExtensions : "mp4" | "webm" | "png" | "jpg" | "gif" | "mkv"



EncoderTarget : "in-browser" | "file-system"



FFmpegEncoderOptions : object



FFmpegEncoderEncoderOptions : module:@ffmpeg/ffmpeg/dist/esm/types.js~FFMessageLoadConfig



GIFEncoderOptions : object



GIFEncoderQuantizeOptions : object



GIFEncoderEncoderOptions : object



H264MP4EncoderOptions : object



H264MP4EncoderEncoderOptions : module:h264-mp4-encoder~H264MP4Encoder



MediaCaptureEncoderOptions : object



MediaCaptureEncoderEncoderOptions : MediaRecorderOptions



MP4WasmEncoderOptions : object



MP4WasmEncoderEncoderOptions : VideoEncoderConfig



WebCodecsEncoderOptions : object



WebCodecsEncoderEncoderOptions : VideoEncoderConfig



WebCodecsMuxerOptions : MuxerOptions


## canvas-record

Re-export Recorder, RecorderStatus, all Encoders and utils.

## Recorder

**Kind**: global class

- [Recorder](#Recorder)
- [new Recorder(context, [options])](#new_Recorder_new)
- [.defaultOptions](#Recorder+defaultOptions) : [RecorderOptions](#RecorderOptions)
- [.mimeTypes](#Recorder+mimeTypes) : object
- [.start([startOptions])](#Recorder+start)
- [.step()](#Recorder+step)
- [.stop()](#Recorder+stop) ⇒ ArrayBuffer \| Uint8Array \| Array.<Blob> \| undefined
- [.dispose()](#Recorder+dispose)

### new Recorder(context, [options])

Create a Recorder instance

| Param | Type | Default |
| --------- | ------------------------------------------------ | --------------- |
| context | RenderingContext | |
| [options] | [RecorderOptions](#RecorderOptions) | {} |

### recorder.defaultOptions : [RecorderOptions](#RecorderOptions)

Sensible defaults for recording so that the recorder "just works".

**Kind**: instance property of [Recorder](#Recorder)

### recorder.mimeTypes : object

A mapping of extension to their mime types

**Kind**: instance property of [Recorder](#Recorder)

### recorder.start([startOptions])

Start the recording by initializing and optionally calling the initial step.

**Kind**: instance method of [Recorder](#Recorder)

| Param | Type | Default |
| -------------- | ---------------------------------------------------------- | --------------- |
| [startOptions] | [RecorderStartOptions](#RecorderStartOptions) | {} |

### recorder.step()

Encode a frame and increment the time and the playhead.
Calls `await canvasRecorder.stop()` when duration is reached.

**Kind**: instance method of [Recorder](#Recorder)

### recorder.stop() ⇒ ArrayBuffer \| Uint8Array \| Array.<Blob> \| undefined

Stop the recording and return the recorded buffer.
If options.download is set, automatically start downloading the resulting file.
Is called when duration is reached or manually.

**Kind**: instance method of [Recorder](#Recorder)

### recorder.dispose()

Clean up the recorder and encoder

**Kind**: instance method of [Recorder](#Recorder)

## Encoder

**Kind**: global class
**Properties**

| Name | Type |
| ---------------- | ---------------------------------------------------- |
| target | [EncoderTarget](#EncoderTarget) |
| extension | [EncoderExtensions](#EncoderExtensions) |
| [encoderOptions] | object |
| [muxerOptions] | object |

- [Encoder](#Encoder)
- [new Encoder(options)](#new_Encoder_new)
- [.supportedExtensions](#Encoder+supportedExtensions) : Array.<Extensions>
- [.supportedTargets](#Encoder+supportedTargets) : [Array.<EncoderTarget>](#EncoderTarget)
- [.init(options)](#Encoder+init)
- [.encode(frame, [frameNumber])](#Encoder+encode)
- [.stop()](#Encoder+stop) ⇒ ArrayBuffer \| Uint8Array \| Array.<Blob> \| undefined
- [.dispose()](#Encoder+dispose)

### new Encoder(options)

Base Encoder class. All Encoders extend it and its method are called by the Recorder.

| Param | Type |
| ------- | ------------------- |
| options | object |

### encoder.supportedExtensions : Array.<Extensions>

The extension the encoder supports

**Kind**: instance property of [Encoder](#Encoder)

### encoder.supportedTargets : [Array.<EncoderTarget>](#EncoderTarget)

The target to download the file to.

**Kind**: instance property of [Encoder](#Encoder)

### encoder.init(options)

Setup the encoder: load binary, instantiate muxers, setup file system target...

**Kind**: instance method of [Encoder](#Encoder)

| Param | Type |
| ------- | ------------------- |
| options | object |

### encoder.encode(frame, [frameNumber])

Encode a single frame. The frameNumber is usually used for GOP (Group Of Pictures).

**Kind**: instance method of [Encoder](#Encoder)

| Param | Type |
| ------------- | ------------------- |
| frame | number |
| [frameNumber] | number |

### encoder.stop() ⇒ ArrayBuffer \| Uint8Array \| Array.<Blob> \| undefined

Stop the encoding process and cleanup the temporary data.

**Kind**: instance method of [Encoder](#Encoder)

### encoder.dispose()

Clean up the encoder

**Kind**: instance method of [Encoder](#Encoder)

## FFmpegEncoder

**Kind**: global class

### new FFmpegEncoder([options])

| Param | Type |
| --------- | ---------------------------------------------------------- |
| [options] | [FFmpegEncoderOptions](#FFmpegEncoderOptions) |

## FrameEncoder

**Kind**: global class

## GIFEncoder

**Kind**: global class

### new GIFEncoder([options])

| Param | Type |
| --------- | ---------------------------------------------------- |
| [options] | [GIFEncoderOptions](#GIFEncoderOptions) |

## H264MP4Encoder

**Kind**: global class

### new H264MP4Encoder([options])

| Param | Type |
| --------- | ------------------------------------------------------------ |
| [options] | [H264MP4EncoderOptions](#H264MP4EncoderOptions) |

## MediaCaptureEncoder

**Kind**: global class

### new MediaCaptureEncoder([options])

| Param | Type |
| --------- | ---------------------------------------------------------------------- |
| [options] | [MediaCaptureEncoderOptions](#MediaCaptureEncoderOptions) |

## MP4WasmEncoder

**Kind**: global class

### new MP4WasmEncoder([options])

| Param | Type |
| --------- | ------------------------------------------------------------ |
| [options] | [MP4WasmEncoderOptions](#MP4WasmEncoderOptions) |

## WebCodecsEncoder

**Kind**: global class

### new WebCodecsEncoder([options])

| Param | Type |
| --------- | ---------------------------------------------------------------- |
| [options] | [WebCodecsEncoderOptions](#WebCodecsEncoderOptions) |

## RecorderStatus : enum

Enum for recorder status

**Kind**: global enum
**Read only**: true
**Example**

```js
// Check recorder status before continuing
if (canvasRecorder.status !== RecorderStatus.Stopped) {
rAFId = requestAnimationFrame(() => tick());
}
```

## isWebCodecsSupported : boolean

Check for WebCodecs support on the current platform.

**Kind**: global constant

## estimateBitRate(width, height, frameRate, motionRank, bitrateMode) ⇒ number

Estimate the bit rate of a video rounded to nearest megabit.
Based on "H.264 for the rest of us" by Kush Amerasinghe.

**Kind**: global function
**Returns**: number - A bitrate value in bits per second

| Param | Type | Default | Description |
| ----------- | ---------------------------------------------------------------------- | --------------------- | --------------------- |
| width | number | | |
| height | number | | |
| frameRate | number | 30 | |
| motionRank | number | 4 | A factor of 1, 2 or 4 |
| bitrateMode | "variable" \| "constant" | variable | |

**Example**

```js
// Full HD (1080p)
const bitRate = estimateBitRate(1920, 1080, 30, "variable");
const bitRateMbps = bitRate * 1_000_000; // => 13 Mbps
```

## onStatusChangeCb : function

A callback to notify on the status change. To compare with RecorderStatus enum values.

**Kind**: global typedef

| Param | Type | Description |
| -------------- | ------------------- | ----------- |
| RecorderStatus | number | the status |

## RecorderOptions : object

Options for recording. All optional.

**Kind**: global typedef
**Properties**

| Name | Type | Default | Description |
| ---------------- | -------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [name] | string | "\"\"" | A name for the recorder, used as prefix for the default file name. |
| [duration] | number | 10 | The recording duration in seconds. If set to Infinity, `await canvasRecorder.stop()` needs to be called manually. |
| [frameRate] | number | 30 | The frame rate in frame per seconds. Use `await canvasRecorder.step();` to go to the next frame. |
| [download] | boolean | true | Automatically download the recording when duration is reached or when `await canvasRecorder.stop()` is manually called. |
| [extension] | string | "\"mp4\"" | Default file extension: infers which Encoder is selected. |
| [target] | string | "\"in-browser\"" | Default writing target: in-browser or file-system when available. |
| [encoder] | object | | A specific encoder. Default encoder based on options.extension: GIF > WebCodecs > H264MP4. |
| [encoderOptions] | object | | See `src/encoders` or individual packages for a list of options. |
| [muxerOptions] | object | | See "mp4-muxer" and "webm-muxer" for a list of options. |
| [onStatusChange] | [onStatusChangeCb](#onStatusChangeCb) | | |

## RecorderStartOptions : object

Options for recording initialisation. All optional.

**Kind**: global typedef
**Properties**

| Name | Type | Description |
| ---------- | -------------------- | ----------------------------------------------------------------------------- |
| [filename] | string | Overwrite the file name completely. |
| [initOnly] | boolean | Only initialised the recorder and don't call the first await recorder.step(). |

## EncoderExtensions : "mp4" \| "webm" \| "png" \| "jpg" \| "gif" \| "mkv"

**Kind**: global typedef

## EncoderTarget : "in-browser" \| "file-system"

**Kind**: global typedef

## FFmpegEncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ---------------- | ------------------------------------------------------------------------ | --------------- |
| [encoderOptions] | [FFmpegEncoderEncoderOptions](#FFmpegEncoderEncoderOptions) | {} |

## FFmpegEncoderEncoderOptions : module:@ffmpeg/ffmpeg/dist/esm/types.js~FFMessageLoadConfig

**Kind**: global typedef
**See**: [FFmpeg#load](https://ffmpegwasm.netlify.app/docs/api/ffmpeg/classes/FFmpeg#load)

## GIFEncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ----------------- | -------------------------------------------------------------------- | ---------------- |
| [maxColors] | number | 256 |
| [quantizeOptions] | [GIFEncoderQuantizeOptions](#GIFEncoderQuantizeOptions) | |
| [encoderOptions] | [GIFEncoderEncoderOptions](#GIFEncoderEncoderOptions) | {} |

## GIFEncoderQuantizeOptions : object

**Kind**: global typedef
**See**: [QuantizeOptions](https://github.com/mattdesl/gifenc#palette--quantizergba-maxcolors-options--)
**Properties**

| Name | Type | Default |
| --------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------- |
| [format] | "rgb565" \| "rgb444" \| "rgba4444" | "rgb565" |
| [oneBitAlpha] | boolean \| number | false |
| [clearAlpha] | boolean | true |
| [clearAlphaThreshold] | number | 0 |
| [clearAlphaColor] | number | 0x00 |

## GIFEncoderEncoderOptions : object

**Kind**: global typedef
**See**: [WriteFrameOpts](https://github.com/mattdesl/gifenc#gifwriteframeindex-width-height-opts--)
**Properties**

| Name | Type | Default |
| ------------------ | ----------------------------------------------- | ------------------ |
| [palette] | Array.<Array.<number>> | |
| [first] | boolean | false |
| [transparent] | boolean | 0 |
| [transparentIndex] | number | 0 |
| [delay] | number | 0 |
| [repeat] | number | 0 |
| [dispose] | number | -1 |

## H264MP4EncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ---------------- | -------------------------------------------------------------------------- | --------------- |
| [debug] | boolean | |
| [encoderOptions] | [H264MP4EncoderEncoderOptions](#H264MP4EncoderEncoderOptions) | {} |

## H264MP4EncoderEncoderOptions : module:h264-mp4-encoder~H264MP4Encoder

**Kind**: global typedef
**See**: [h264-mp4-encoder#api](https://github.com/TrevorSundberg/h264-mp4-encoder#api)

## MediaCaptureEncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ---------------- | ------------------------------------------------------------------------------------ | --------------- |
| [flushFrequency] | number | 10 |
| [encoderOptions] | [MediaCaptureEncoderEncoderOptions](#MediaCaptureEncoderEncoderOptions) | {} |

## MediaCaptureEncoderEncoderOptions : MediaRecorderOptions

**Kind**: global typedef
**See**: [MediaRecorder#options](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#options)

## MP4WasmEncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ----------------- | -------------------------------------------------------------------------- | --------------- |
| [groupOfPictures] | number | 20 |
| [flushFrequency] | number | 10 |
| [encoderOptions] | [MP4WasmEncoderEncoderOptions](#MP4WasmEncoderEncoderOptions) | {} |

## MP4WasmEncoderEncoderOptions : VideoEncoderConfig

**Kind**: global typedef
**See**: [VideoEncoder.configure](https://developer.mozilla.org/en-US/docs/Web/API/VideoEncoder/configure#config)

## WebCodecsEncoderOptions : object

**Kind**: global typedef
**Properties**

| Name | Type | Default |
| ----------------- | ------------------------------------------------------------------------------ | --------------- |
| [groupOfPictures] | number | 20 |
| [flushFrequency] | number | 10 |
| [encoderOptions] | [WebCodecsEncoderEncoderOptions](#WebCodecsEncoderEncoderOptions) | {} |

## WebCodecsEncoderEncoderOptions : VideoEncoderConfig

**Kind**: global typedef
**See**: [VideoEncoder.configure](https://developer.mozilla.org/en-US/docs/Web/API/VideoEncoder/configure#config)

## WebCodecsMuxerOptions : MuxerOptions

**Kind**: global typedef
**See**

- [Mp4.MuxerOptions](https://github.com/Vanilagy/mp4-muxer/#usage)
- [WebM.MuxerOptions](https://github.com/Vanilagy/webm-muxer/#usage)

## License

All MIT:

- [mp4-wasm](https://github.com/mattdesl/mp4-wasm/blob/master/LICENSE.md)
- [h264-mp4-encoder](https://github.com/TrevorSundberg/h264-mp4-encoder/blob/master/LICENSE.md)
- [@ffmpeg/ffmpeg](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/LICENSE)
- [gifenc](https://github.com/mattdesl/gifenc/blob/master/LICENSE.md)
- [webm-muxer](https://github.com/Vanilagy/webm-muxer/blob/main/LICENSE)
- [mp4-muxer](https://github.com/Vanilagy/mp4-muxer/blob/main/LICENSE)

MIT. See [license file](https://github.com/dmnsgn/canvas-record/blob/main/LICENSE.md).