
Security News
/Research
Popular node-ipc npm Package Infected with Credential Stealer
Socket detected malicious node-ipc versions with obfuscated stealer/backdoor behavior in a developing npm supply chain attack.
webcodecs-encoder
Advanced tools
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.
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.
encode(), encodeStream(), and canEncode() functionslow, medium, high, lossless presetsencodeStream()sideEffects: false for efficient bundling.video: false option (v0.2.2)npm install webcodecs-encoder
# or
yarn add webcodecs-encoder
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:
NODE_ENV=test) or when you explicitly opt in.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). |
WEBCODECS_WORKER_URL=/assets/webcodecs-worker.js or window.__WEBCODECS_WORKER_URL__ = '/assets/webcodecs-worker.js' | Override the external worker URL when your app is served from a sub-path/CDN. |
β οΈ 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/
// 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;
}
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);
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' });
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);
}
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'
});
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>
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;
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';
codecString?: string; // e.g. 'avc1.640028'
bitrate?: number;
quantizer?: number;
avc?: { format?: 'annexb' | 'avc' };
hevc?: { format?: 'annexb' | 'hevc' };
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
keyFrameInterval?: number;
} | false;
/** Set to `false` to disable audio. */
audio?: {
codec?: 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac';
codecString?: string; // e.g. 'mp4a.40.2'
bitrate?: number;
sampleRate?: number;
channels?: number;
bitrateMode?: 'constant' | 'variable';
aac?: { format?: 'aac' | 'adts' };
} | 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.
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:
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();
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`);
}
}
});
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;
}
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
});
The main package entry webcodecs-encoder exports all core functionalities. Sub-path imports like webcodecs-encoder/factory are no longer necessary.
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);
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'.
dom.media.webcodecs.enabled).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
}
{ video: { hardwareAcceleration: 'prefer-hardware' } }.encodeStream() instead of encode().encode(stream, {
latencyMode: 'realtime',
maxVideoQueueSize: 15, // Lower queue size for lower latency
backpressureStrategy: 'drop' // Drop frames if the system can't keep up
});
Breaking Changes: The MediaStreamRecorder class has been removed in favor of the function-first API.
import { MediaStreamRecorder } from 'webcodecs-encoder';
const recorder = new MediaStreamRecorder(options);
await recorder.start(stream);
// ... recording in progress
const mp4Data = await recorder.stop();
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;
}
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.
π Major Features
encodeStream() MediaStream processing - no longer throws errorsvideo: false option support for pure audio encodingπ§ Improvements
video: false configurationsπ Bug Fixes
encodeStream()π Documentation
MIT License - see LICENSE file for details.
Contributions are welcome! Please see our Contributing Guide for details.
FAQs
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.
The npm package webcodecs-encoder receives a total of 14 weekly downloads. As such, webcodecs-encoder popularity was classified as not popular.
We found that webcodecs-encoder demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
/Research
Socket detected malicious node-ipc versions with obfuscated stealer/backdoor behavior in a developing npm supply chain attack.

Security News
TeamPCP and BreachForums are promoting a Shai-Hulud supply chain attack contest with a $1,000 prize for the biggest package compromise.

Security News
Packagist urges PHP projects to update Composer after a GitHub token format change exposed some GitHub Actions tokens in CI logs.