webcodecs-encoder
v0.3.1
Published
A TypeScript library for browser environments to encode video (H.264/AVC, VP9, VP8) and audio (AAC, Opus) using the WebCodecs API and mux them into MP4 or WebM containers with real-time streaming support. New function-first API design.
Maintainers
Readme
WebCodecs Encoder
Function-First API to encode video and audio using WebCodecs API.
A TypeScript library to encode video (H.264/AVC, HEVC, VP9, VP8, AV1) and audio (AAC, MP3, Opus, Vorbis, FLAC) using the WebCodecs API and mux them into MP4 or WebM containers with a simple, function-first API.
Features
- 🚀 Function-First API: Simple
encode(),encodeStream(), andcanEncode()functions - 🎯 Zero Configuration: Automatic resolution, frame rate, and codec detection
- 📊 Quality Presets: Simple
low,medium,high,losslesspresets - 🔄 Multiple Input Types: Frame arrays, AsyncIterable, MediaStream, VideoFile
- ⚡ Real-time Streaming: Progressive encoding with
encodeStream() - 🎨 Progressive Enhancement: Start simple, add complexity as needed
- 📦 Optimized Bundle Size: Tree-shakable with ES Modules and
sideEffects: falsefor efficient bundling. - 🛡️ Type Safety: Full TypeScript support with comprehensive types
- 🎵 Audio Support: Automatic AAC↔MP3 fallback for MP4 and Opus/Vorbis/FLAC support for WebM
- 🎤 Audio-Only Encoding: Support for
video: falseoption (v0.2.2) - 📹 VideoFile Audio: Extract and encode audio from video files (v0.2.2)
- ⚡ Performance Optimized: Transferable objects for faster data transfer (v0.2.2)
Installation
npm install webcodecs-encoder
# or
yarn add webcodecs-encoderWorker Setup
The encoder runs inside a dedicated Web Worker (/webcodecs-worker.js). Ship that file with your app and ensure it is publicly reachable at the site root.
By default the library:
- Prefers the external worker in browsers and production builds.
- Falls back to an inline mock only when running under known test runners (Vitest, Jest,
NODE_ENV=test) or when you explicitly opt in. - Never uses the inline mock in production unless you override the safety check.
Inline worker controls:
| Flag | Effect |
| --- | --- |
| WEBCODECS_USE_INLINE_WORKER=true or window.__WEBCODECS_USE_INLINE_WORKER__ = true | Force the inline mock (useful for Storybook, unit tests, etc.). |
| WEBCODECS_DISABLE_INLINE_WORKER=true or window.__WEBCODECS_DISABLE_INLINE_WORKER__ = true | Always require the external worker. |
| WEBCODECS_ALLOW_INLINE_IN_PROD=true or window.__WEBCODECS_ALLOW_INLINE_IN_PROD__ = true | Explicitly permit the inline mock on production builds (not recommended). |
⚠️ The inline worker is a test stub that returns placeholder bytes. Use it only for wiring/UI development. Real MP4/WebM output requires the external worker bundle.
Copy the worker file from node_modules into your public assets directory during build/deploy:
# Example for a Next.js/Vite project with a 'public' directory
cp node_modules/webcodecs-encoder/dist/webcodecs-worker.js public/Example: Vite + TypeScript
// vite.config.ts
import { defineConfig } from 'vite';
import copy from 'rollup-plugin-copy';
export default defineConfig({
plugins: [
copy({
targets: [
{
src: 'node_modules/webcodecs-encoder/dist/webcodecs-worker.js',
dest: 'public'
}
]
})
]
});// main.ts (development helper)
if (import.meta.env.DEV) {
window.__WEBCODECS_USE_INLINE_WORKER__ = true;
}Quick Start
Basic Encoding
import { encode } from 'webcodecs-encoder';
// Encode frames with automatic configuration
const frames = [/* VideoFrame, Canvas, ImageData objects */];
const mp4Data = await encode(frames, { quality: 'medium' });
// Save or use the encoded MP4
const blob = new Blob([mp4Data], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);Audio-Only Encoding
import { encode } from 'webcodecs-encoder';
// Encode a microphone stream to an Opus audio file in a WebM container
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const webmAudio = await encode(micStream, {
video: false, // Required for audio-only
audio: {
codec: 'opus',
bitrate: 128_000
},
container: 'webm'
});
const blob = new Blob([webmAudio], { type: 'audio/webm' });Streaming Encoding
import { encodeStream } from 'webcodecs-encoder';
// Real-time encoding for live streaming
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
for await (const chunk of encodeStream(stream, { quality: 'high' })) {
// Send chunk to MediaSource, server, or save incrementally
mediaSource.appendBuffer(chunk);
}Check Browser Support
import { canEncode } from 'webcodecs-encoder';
// Check if encoding is supported
const isSupported = await canEncode();
// Check specific configuration
const canEncodeHEVC = await canEncode({
video: { codec: 'hevc' },
quality: 'high'
});API Reference
Core Functions
encode(source, options?)
Encode video to a complete MP4/WebM file.
async function encode(
source: VideoSource,
options?: EncodeOptions
): Promise<Uint8Array>encodeStream(source, options?)
Stream encoding with real-time chunks.
async function* encodeStream(
source: VideoSource,
options?: EncodeOptions
): AsyncGenerator<Uint8Array>canEncode(options?)
Check if encoding is supported with given options.
async function canEncode(options?: EncodeOptions): Promise<boolean>Video Sources
The API supports multiple input types:
type VideoSource =
| Frame[] // Static frame array
| AsyncIterable<Frame> // Dynamic frame generation
| MediaStream // Camera/screen capture
| VideoFile; // Existing video file
type Frame = VideoFrame | HTMLCanvasElement | OffscreenCanvas | ImageBitmap | ImageData;Encode Options
interface EncodeOptions {
// Basic settings (auto-detected if not specified)
width?: number;
height?: number;
frameRate?: number;
// Quality preset (recommended)
quality?: 'low' | 'medium' | 'high' | 'lossless';
// Advanced settings
/** Set to `false` for audio-only encoding. */
video?: {
codec?: 'avc' | 'hevc' | 'vp9' | 'vp8' | 'av1';
bitrate?: number;
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
keyFrameInterval?: number;
} | false;
/** Set to `false` to disable audio. */
audio?: {
codec?: 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac';
bitrate?: number;
sampleRate?: number;
channels?: number;
bitrateMode?: 'constant' | 'variable';
} | false;
container?: 'mp4' | 'webm';
// --- Advanced Control ---
/**
* Latency mode for encoder and muxer.
* `encodeStream()` automatically uses 'realtime'.
*/
latencyMode?: 'quality' | 'realtime';
/**
* How to handle the first timestamp. 'offset' is recommended for streams.
* - 'offset': Shifts all timestamps so the first one is zero.
* - 'strict': Uses the original timestamps.
*/
firstTimestampBehavior?: 'offset' | 'strict';
/**
* Maximum video encode queue size before applying backpressure (default: 30).
*/
maxVideoQueueSize?: number;
/**
* Maximum audio encode queue size before applying backpressure (default: 30).
*/
maxAudioQueueSize?: number;
/**
* Strategy for handling encode queue overflow (default: 'drop').
* - 'drop': Discard new frames when the queue is full.
* - 'wait': Block the processing loop until there is space in the queue.
*/
backpressureStrategy?: 'drop' | 'wait';
// Callbacks
onProgress?: (progress: ProgressInfo) => void;
onError?: (error: EncodeError) => void;
}
interface ProgressInfo {
/** Percentage of completion (0-100) */
percent: number;
/** Total number of frames processed */
processedFrames: number;
/** Total number of frames to encode (if known) */
totalFrames?: number;
/** Current encoding speed in frames per second */
fps: number;
/** Current stage label ("streaming", "finalizing", etc.) */
stage: string;
/** Estimated remaining time in milliseconds */
estimatedRemainingMs?: number;
}Audio codec compatibility
container: 'mp4'supportsaac(default) and automatically falls back tomp3if AAC isn’t available.container: 'webm'supportsopus(default) withvorbisandflacas fallbacks.- Other codec hints are treated as best-effort; if they can’t be muxed into the requested container the encoder switches to the first compatible alternative.
Real-time MediaStream Recording
Use encodeStream() for real-time recording from camera, microphone, or screen sharing. This provides progressive encoding with streaming output:
import { encodeStream } from 'webcodecs-encoder';
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// Collect encoded chunks as they're generated
const chunks: Uint8Array[] = [];
for await (const chunk of encodeStream(stream, {
quality: 'medium',
container: 'mp4',
onProgress: (progress) => {
console.log(`Recording: ${progress.percent.toFixed(1)}%`);
}
})) {
chunks.push(chunk);
console.log(`Received chunk: ${chunk.byteLength} bytes`);
// Optional: Send chunks to server for real-time streaming
// await sendChunkToServer(chunk);
}
// Stop recording by stopping the media tracks
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
}, 5000); // Record for 5 seconds
// Combine chunks into final video file
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const finalVideo = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
finalVideo.set(chunk, offset);
offset += chunk.byteLength;
}
const blob = new Blob([finalVideo], { type: 'video/mp4' });Real-time streaming benefits:
- Progressive encoding as data flows
- Immediate chunk availability for streaming
- Built-in cancellation support
- Memory efficient for long recordings
Usage Examples
1. Canvas Animation to MP4
import { encode } from 'webcodecs-encoder';
// Create animation frames
const frames = [];
const canvas = new OffscreenCanvas(800, 600);
const ctx = canvas.getContext('2d');
for (let i = 0; i < 120; i++) { // 4 seconds at 30fps
ctx.clearRect(0, 0, 800, 600);
ctx.fillStyle = `hsl(${i * 3}, 70%, 50%)`;
ctx.fillRect(i * 6, 200, 100, 200);
frames.push(canvas.transferToImageBitmap());
}
// Encode with automatic settings
const mp4 = await encode(frames, {
quality: 'high',
frameRate: 30
});
// Save the file
const blob = new Blob([mp4], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'animation.mp4';
a.click();2. Camera Recording with Progress
import { encode } from 'webcodecs-encoder';
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
const mp4 = await encode(stream, {
quality: 'medium',
container: 'mp4',
onProgress: (progress) => {
console.log(`Progress: ${progress.percent.toFixed(1)}%`);
console.log(`Speed: ${progress.fps.toFixed(1)} fps`);
if (progress.estimatedRemainingMs) {
console.log(`ETA: ${(progress.estimatedRemainingMs / 1000).toFixed(1)}s`);
}
}
});3. Real-time Streaming
import { encodeStream } from 'webcodecs-encoder';
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const chunks = [];
for await (const chunk of encodeStream(stream, {
quality: 'medium',
video: { latencyMode: 'realtime' }
})) {
// Send to server or MediaSource immediately
chunks.push(chunk);
// Or stream to MediaSource Extensions
if (mediaSource.readyState === 'open') {
sourceBuffer.appendBuffer(chunk);
}
}
// Combine all chunks for final file
const fullVideo = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
fullVideo.set(chunk, offset);
offset += chunk.length;
}4. Custom Frame Generation
import { encode } from 'webcodecs-encoder';
// Generate frames dynamically
async function* generateFrames() {
const canvas = new OffscreenCanvas(640, 480);
const ctx = canvas.getContext('2d');
for (let frame = 0; frame < 300; frame++) { // 10 seconds at 30fps
// Draw your animation
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 640, 480);
ctx.fillStyle = '#fff';
ctx.font = '48px Arial';
ctx.fillText(`Frame ${frame}`, 50, 240);
yield canvas.transferToImageBitmap();
// Optional: add timing control
await new Promise(resolve => setTimeout(resolve, 33)); // ~30fps
}
}
const mp4 = await encode(generateFrames(), {
quality: 'high',
frameRate: 30
});Advanced Usage
The main package entry webcodecs-encoder exports all core functionalities. Sub-path imports like webcodecs-encoder/factory are no longer necessary.
Custom Encoder Factory
For repeated encoding with the same settings:
import { createEncoder, encoders } from 'webcodecs-encoder';
// Create custom encoder
const myEncoder = createEncoder({
quality: 'high',
video: { codec: 'avc' },
audio: { codec: 'aac', bitrate: 192_000 }
});
// Use multiple times
const video1 = await myEncoder.encode(frames1);
const video2 = await myEncoder.encode(frames2);
// Or use predefined encoders
const youtubeVideo = await encoders.youtube.encode(frames);
const twitterVideo = await encoders.twitter.encode(frames);Error Handling
import { encode, EncodeError } from 'webcodecs-encoder';
try {
const mp4 = await encode(frames, { quality: 'high' });
} catch (error) {
if (error instanceof EncodeError) {
// The 'type' property provides specific details
console.error(`Encoding failed: ${error.type}`, error.message);
switch (error.type) {
case 'not-supported':
console.log('WebCodecs not supported in this browser');
break;
case 'invalid-input':
console.log('Invalid input frames or configuration');
break;
case 'configuration-error':
console.log('The provided configuration is not supported.');
break;
case 'initialization-failed':
case 'video-encoding-error':
case 'audio-encoding-error':
case 'muxing-failed':
case 'worker-error':
console.log('A critical error occurred during the encoding process.');
break;
case 'cancelled':
console.log('The encoding was cancelled.');
break;
// ... handle other specific error types
default:
console.log('Unknown encoding error:', error.message);
}
}
}The EncodeError.type can be one of: 'not-supported', 'invalid-input', 'initialization-failed', 'configuration-error', 'video-encoding-error', 'audio-encoding-error', 'muxing-failed', 'cancelled', 'timeout', 'worker-error', 'filesystem-error', 'unknown'.
Browser Support
- Chrome 113+: Full support.
- Edge 113+: Full support.
- Firefox: Experimental support (enable
dom.media.webcodecs.enabled). - Safari: Not yet supported.
Note: While WebCodecs was available in earlier versions, versions 113+ are recommended for stability.
Check support at runtime:
import { canEncode } from 'webcodecs-encoder';
const supported = await canEncode();
if (!supported) {
// Fallback to MediaRecorder or other solutions
}Performance Tips
- Use quality presets instead of manual bitrate calculation.
- Enable hardware acceleration when available:
{ video: { hardwareAcceleration: 'prefer-hardware' } }. - Use streaming for large videos:
encodeStream()instead ofencode(). - Optimize frame rate for your use case (30fps is usually sufficient).
- Tune queue limits for real-time streams: Adjust queue size and backpressure strategy to balance latency and frame drops.
encode(stream, { latencyMode: 'realtime', maxVideoQueueSize: 15, // Lower queue size for lower latency backpressureStrategy: 'drop' // Drop frames if the system can't keep up }); - Consider container format: MP4 for compatibility, WebM for smaller files.
Migration Guide
From v0.2.x to v0.3.0
Breaking Changes: The MediaStreamRecorder class has been removed in favor of the function-first API.
Before (v0.2.x)
import { MediaStreamRecorder } from 'webcodecs-encoder';
const recorder = new MediaStreamRecorder(options);
await recorder.start(stream);
// ... recording in progress
const mp4Data = await recorder.stop();After (v0.3.0+)
import { encodeStream } from 'webcodecs-encoder';
const chunks: Uint8Array[] = [];
for await (const chunk of encodeStream(stream, options)) {
chunks.push(chunk);
}
// Combine chunks into final video
const totalSize = chunks.reduce((sum, c) => sum + c.byteLength, 0);
const mp4Data = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
mp4Data.set(chunk, offset);
offset += chunk.byteLength;
}Migration Benefits
- Better tree-shaking: Smaller bundle sizes with function imports
- Streaming support: Real-time chunk processing
- Memory efficiency: Progressive encoding without buffering entire video
- Error handling: Standard async/await error handling
Common Migration Patterns
Recording Control:
// Before: recorder.start() / recorder.stop()
// After: Control via MediaStream tracks
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
}, 5000);Progress Tracking:
// Before: constructor options
new MediaStreamRecorder({ onProgress })
// After: encodeStream options
encodeStream(stream, { onProgress })Cancellation:
// Before: recorder.cancel()
// After: Stop MediaStream tracks or break out of the loop
const stopRecording = () => {
stream.getTracks().forEach(track => track.stop());
};
setTimeout(stopRecording, 5000);The current implementation does not accept an
AbortSignal. To cancelencode/encodeStream, stop the MediaStream tracks or end the async generator manually.
See examples/realtime-mediastream.ts for complete examples.
Changelog
v0.2.2 (2025-06-14)
🚀 Major Features
- Real-time streaming: Fixed
encodeStream()MediaStream processing - no longer throws errors - Audio-only encoding: Added
video: falseoption support for pure audio encoding - VideoFile audio extraction: Automatic audio track processing from video files using AudioContext
- Transferable objects optimization: Improved performance with optimized VideoFrame/AudioData transfer
🔧 Improvements
- Enhanced MediaStream track detection for audio-only streams
- Better error handling for AudioContext unavailability
- Optimized worker communication with transferable objects
- Extended type definitions for
video: falseconfigurations
🐛 Bug Fixes
- Fixed real-time MediaStream processing in
encodeStream() - Resolved audio processing issues in VideoFile inputs
- Improved configuration inference for audio-only scenarios
📝 Documentation
- Added comprehensive examples for new features
- Updated API documentation with v0.2.2 features
- Added performance optimization guidelines
v0.2.1 (2025-06-13)
- Added VideoFile support and removed AudioWorklet feature
- Updated MediaStreamRecorder to use MediaStreamTrackProcessor
- Improved build configuration and exports
- Enhanced test coverage and documentation
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
