webcodecs-encoder
Version:
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.
421 lines (319 loc) • 11.8 kB
Markdown
A TypeScript library 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 a simple, function-first design.
> **🎉 v0.2.2 Release**
> Major updates: Real-time streaming support, audio-only encoding, VideoFile audio processing, and optimized transferable objects. See [CHANGELOG](#changelog) for details.
## Features
- **🚀 Function-First API**: Simple `encode()`, `encodeStream()`, and `canEncode()` functions
- **🎯 Zero Configuration**: Automatic resolution, frame rate, and codec detection
- **📊 Quality Presets**: Simple `low`, `medium`, `high`, `lossless` presets
- **🔄 Multiple Input Types**: Frame arrays, AsyncIterable, MediaStream, VideoFile
- **⚡ Real-time Streaming**: Progressive encoding with `encodeStream()`
- **🎨 Progressive Enhancement**: Start simple, add complexity as needed
- **🔧 Transparent Worker Management**: No manual worker setup required
- **📦 Optimized Bundle Size**: Import only what you need
- **🛡️ Type Safety**: Full TypeScript support with comprehensive types
- **🎵 Audio Support**: AAC and Opus encoding with automatic configuration
- **🎤 Audio-Only Encoding**: Support for `video: false` option (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
```bash
npm install webcodecs-encoder
# or
yarn add webcodecs-encoder
```
No additional setup required! The library automatically manages Web Workers internally.
## Quick Start
### Basic Encoding
```typescript
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);
```
```typescript
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);
}
```
```typescript
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 video to a complete MP4/WebM file.
```typescript
async function encode(
source: VideoSource,
options?: EncodeOptions
): Promise<Uint8Array>
```
#### `encodeStream(source, options?)`
Stream encoding with real-time chunks.
```typescript
async function* encodeStream(
source: VideoSource,
options?: EncodeOptions
): AsyncGenerator<Uint8Array>
```
#### `canEncode(options?)`
Check if encoding is supported with given options.
```typescript
async function canEncode(options?: EncodeOptions): Promise<boolean>
```
### Video Sources
The API supports multiple input types:
```typescript
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;
```
```typescript
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
video?: {
codec?: 'avc' | 'hevc' | 'vp9' | 'vp8' | 'av1';
bitrate?: number;
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
latencyMode?: 'quality' | 'realtime';
keyFrameInterval?: number;
};
audio?: {
codec?: 'aac' | 'opus';
bitrate?: number;
sampleRate?: number;
channels?: number;
bitrateMode?: 'constant' | 'variable';
} | false; // false to disable audio
container?: 'mp4' | 'webm';
// Callbacks
onProgress?: (progress: ProgressInfo) => void;
onError?: (error: EncodeError) => void;
}
```
```typescript
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();
```
```typescript
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`);
}
}
});
```
```typescript
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;
}
```
```typescript
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
});
```
For repeated encoding with the same settings:
```typescript
import { createEncoder, encoders } from 'webcodecs-encoder/factory';
// 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);
```
```typescript
import { examples } from 'webcodecs-encoder/factory';
// Optimize for specific platforms
const youtubeEncoder = examples.getEncoderForPlatform('youtube');
const twitterEncoder = examples.getEncoderForPlatform('twitter');
// Resolution-based optimization
const hdEncoder = examples.createByResolution(1920, 1080);
const mobileEncoder = examples.createByResolution(640, 480);
// File size constraints
const smallFileEncoder = examples.createForFileSize(10, 60); // 10MB for 60 seconds
```
```typescript
import { encode, EncodeError } from 'webcodecs-encoder';
try {
const mp4 = await encode(frames, { quality: 'high' });
} catch (error) {
if (error instanceof EncodeError) {
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 'encoding-failed':
console.log('Encoding process failed:', error.message);
break;
default:
console.log('Unknown encoding error:', error.message);
}
}
}
```
- **Chrome 94+**: Full support
- **Edge 94+**: Full support
- **Firefox**: Experimental support (enable `dom.media.webcodecs.enabled`)
- **Safari**: Not yet supported
Check support at runtime:
```typescript
import { canEncode } from 'webcodecs-encoder';
const supported = await canEncode();
if (!supported) {
// Fallback to MediaRecorder or other solutions
}
```
1. **Use quality presets** instead of manual bitrate calculation
2. **Enable hardware acceleration** when available: `{ video: { hardwareAcceleration: 'prefer-hardware' } }`
3. **Use streaming** for large videos: `encodeStream()` instead of `encode()`
4. **Optimize frame rate** for your use case (30fps is usually sufficient)
5. **Consider container format**: MP4 for compatibility, WebM for smaller files
## Changelog
### v0.2.2 (2025-01-14)
**🚀 Major Features**
- **Real-time streaming**: Fixed `encodeStream()` MediaStream processing - no longer throws errors
- **Audio-only encoding**: Added `video: false` option 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: false` configurations
**🐛 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-01-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](LICENSE) file for details.
## Contributing
Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
## Support
- 📖 [Documentation](https://github.com/romot-co/webcodecs-encoder/wiki)
- 🐛 [Issue Tracker](https://github.com/romot-co/webcodecs-encoder/issues)
- 💬 [Discussions](https://github.com/romot-co/webcodecs-encoder/discussions)