@twick/browser-render 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ # Audio Implementation Guide
2
+
3
+ ## Overview
4
+
5
+ Browser-based audio processing for Twick video rendering, matching the server's FFmpeg implementation.
6
+
7
+ ## Architecture
8
+
9
+ ### 1. **Audio Processor** (`src/audio/audio-processor.ts`)
10
+ - Mirrors server's `generate-audio.ts` logic
11
+ - Uses Web Audio API instead of FFmpeg
12
+ - Handles:
13
+ - Asset tracking across frames
14
+ - Audio extraction from video/audio elements
15
+ - Playback rate adjustment
16
+ - Volume control
17
+ - Audio trimming
18
+ - Multi-track mixing
19
+
20
+ ### 2. **Service Worker** (`public/audio-worker.js`)
21
+ - Caches media assets for offline processing
22
+ - Handles background audio extraction
23
+ - Reduces network requests during rendering
24
+
25
+ ### 3. **Audio/Video Muxer** (`src/audio/audio-video-muxer.ts`)
26
+ - Combines video and audio tracks
27
+ - Two approaches:
28
+ - **mp4box.js**: Lightweight, browser-native
29
+ - **FFmpeg.wasm**: More robust, ~30MB bundle
30
+
31
+ ## Implementation Steps
32
+
33
+ ### Step 1: Install Dependencies
34
+
35
+ ```bash
36
+ # For mp4box.js approach (recommended)
37
+ npm install mp4box
38
+
39
+ # OR for FFmpeg.wasm approach (more features, larger)
40
+ npm install @ffmpeg/ffmpeg @ffmpeg/util
41
+ ```
42
+
43
+ ### Step 2: Register Service Worker
44
+
45
+ ```typescript
46
+ // In your app's initialization
47
+ if ('serviceWorker' in navigator) {
48
+ navigator.serviceWorker.register('/audio-worker.js')
49
+ .then(reg => console.log('Audio worker registered'))
50
+ .catch(err => console.error('Audio worker failed:', err));
51
+ }
52
+ ```
53
+
54
+ ### Step 3: Enable Audio in Browser Renderer
55
+
56
+ Update `browser-renderer.ts`:
57
+
58
+ ```typescript
59
+ import { BrowserAudioProcessor, getAssetPlacement } from './audio/audio-processor';
60
+ import { muxAudioVideo } from './audio/audio-video-muxer';
61
+
62
+ // In BrowserWasmExporter.generateAudio():
63
+ public async generateAudio(
64
+ assets: any[][],
65
+ startFrame: number,
66
+ endFrame: number,
67
+ ): Promise<ArrayBuffer | null> {
68
+ const processor = new BrowserAudioProcessor();
69
+ const assetPlacements = getAssetPlacement(assets);
70
+
71
+ const processedBuffers: AudioBuffer[] = [];
72
+ for (const asset of assetPlacements) {
73
+ if (asset.volume > 0 && asset.playbackRate > 0) {
74
+ const buffer = await processor.processAudioAsset(
75
+ asset,
76
+ this.settings.fps || 30,
77
+ endFrame - startFrame
78
+ );
79
+ processedBuffers.push(buffer);
80
+ }
81
+ }
82
+
83
+ const mixedBuffer = processor.mixAudioBuffers(processedBuffers);
84
+ const wavData = processor.audioBufferToWav(mixedBuffer);
85
+
86
+ await processor.close();
87
+ return wavData;
88
+ }
89
+ ```
90
+
91
+ ### Step 4: Collect Audio Assets During Rendering
92
+
93
+ In `renderTwickVideoInBrowser()`:
94
+
95
+ ```typescript
96
+ // Track media assets for each frame
97
+ const mediaAssets: AssetInfo[][] = [];
98
+
99
+ for (let frame = 0; frame < totalFrames; frame++) {
100
+ // ... existing rendering code ...
101
+
102
+ // Collect media assets from current scene
103
+ const currentAssets = (renderer as any).playback.currentScene.getMediaAssets?.() || [];
104
+ mediaAssets.push(currentAssets);
105
+ }
106
+
107
+ // Generate audio after video rendering
108
+ const audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);
109
+
110
+ // Mux audio and video
111
+ if (audioData) {
112
+ const finalBlob = await muxAudioVideo({
113
+ videoBlob,
114
+ audioBuffer: audioData,
115
+ fps,
116
+ width,
117
+ height
118
+ });
119
+ return finalBlob;
120
+ }
121
+ ```
122
+
123
+ ## API Parity with Server
124
+
125
+ | Feature | Server (FFmpeg) | Browser (Web Audio) | Status |
126
+ |---------|-----------------|---------------------|--------|
127
+ | Asset Tracking | `getAssetPlacement()` | `getAssetPlacement()` | ✅ Ready |
128
+ | Audio Extraction | FFmpeg decode | `decodeAudioData()` | ✅ Ready |
129
+ | Playback Rate | `atempo` filter | Sample interpolation | ✅ Ready |
130
+ | Volume | `volume` filter | Gain multiplication | ✅ Ready |
131
+ | Trimming | `atrim` filter | Sample slicing | ✅ Ready |
132
+ | Mixing | `amix` filter | Buffer mixing | ✅ Ready |
133
+ | WAV Encoding | FFmpeg encode | Manual WAV encoding | ✅ Ready |
134
+ | Muxing | FFmpeg merge | mp4box.js / FFmpeg.wasm | ⚠️ Needs library |
135
+
136
+ ## Performance Considerations
137
+
138
+ ### Memory Usage
139
+ - Web Audio API decodes entire audio files into memory
140
+ - Large video files can cause memory issues
141
+ - Consider chunked processing for long videos
142
+
143
+ ### Processing Time
144
+ - Audio processing adds 20-50% to render time
145
+ - Service worker caching helps with repeated renders
146
+ - Consider showing separate progress for video/audio
147
+
148
+ ### Browser Limits
149
+ - Chrome: ~2GB audio buffer limit
150
+ - Safari: Stricter memory limits
151
+ - Firefox: Better memory management but slower
152
+
153
+ ## Example Usage
154
+
155
+ ```typescript
156
+ import { useBrowserRenderer } from '@twick/browser-render';
157
+
158
+ function VideoRenderer() {
159
+ const { render, progress } = useBrowserRenderer({
160
+ width: 1920,
161
+ height: 1080,
162
+ fps: 30,
163
+ includeAudio: true, // Enable audio processing
164
+ });
165
+
166
+ const handleRender = async () => {
167
+ const videoBlob = await render({
168
+ input: {
169
+ properties: { width: 1920, height: 1080, fps: 30 },
170
+ tracks: [
171
+ {
172
+ type: 'element',
173
+ elements: [
174
+ {
175
+ type: 'video',
176
+ src: 'https://example.com/video.mp4',
177
+ // Audio will be automatically extracted and included
178
+ }
179
+ ]
180
+ }
181
+ ]
182
+ }
183
+ });
184
+ };
185
+
186
+ return <button onClick={handleRender}>Render with Audio</button>;
187
+ }
188
+ ```
189
+
190
+ ## Troubleshooting
191
+
192
+ ### No Audio in Output
193
+ 1. Check if `includeAudio: true` is set
194
+ 2. Verify service worker is registered
195
+ 3. Check browser console for Web Audio API errors
196
+ 4. Ensure video sources have audio tracks
197
+
198
+ ### Memory Errors
199
+ 1. Reduce video quality/resolution
200
+ 2. Process shorter segments
201
+ 3. Clear service worker cache
202
+ 4. Use FFmpeg.wasm with streaming
203
+
204
+ ### Performance Issues
205
+ 1. Use service worker caching
206
+ 2. Reduce number of audio tracks
207
+ 3. Lower audio sample rate (default: 48kHz)
208
+ 4. Consider server-side rendering for production
209
+
210
+ ## Future Enhancements
211
+
212
+ - [ ] Streaming audio processing (chunks)
213
+ - [ ] Web Workers for parallel processing
214
+ - [ ] Real-time audio preview
215
+ - [ ] Audio effects (reverb, EQ, etc.)
216
+ - [ ] WASM-based audio processing for better performance
217
+ - [ ] Support for more audio formats
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @twick/browser-render
2
+
3
+ Browser-native video rendering for Twick using WebCodecs API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @twick/browser-render
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Example
14
+
15
+ ```typescript
16
+ import { renderTwickVideoInBrowser } from '@twick/browser-render';
17
+
18
+ const videoBlob = await renderTwickVideoInBrowser({
19
+ variables: {
20
+ input: {
21
+ properties: { width: 1920, height: 1080, fps: 30 },
22
+ tracks: [
23
+ // Your tracks configuration
24
+ ]
25
+ }
26
+ },
27
+ settings: {
28
+ width: 1920,
29
+ height: 1080,
30
+ fps: 30,
31
+ onProgress: (progress) => console.log(`${(progress * 100).toFixed(0)}%`)
32
+ }
33
+ });
34
+
35
+ // Download the video
36
+ const url = URL.createObjectURL(videoBlob);
37
+ const a = document.createElement('a');
38
+ a.href = url;
39
+ a.download = 'video.mp4';
40
+ a.click();
41
+ ```
42
+
43
+ ### React Hook
44
+
45
+ ```tsx
46
+ import { useBrowserRenderer } from '@twick/browser-render';
47
+
48
+ function VideoRenderer() {
49
+ const { render, progress, isRendering, videoBlob, download } = useBrowserRenderer({
50
+ width: 1920,
51
+ height: 1080,
52
+ fps: 30,
53
+ autoDownload: false
54
+ });
55
+
56
+ const handleRender = async () => {
57
+ await render({
58
+ input: {
59
+ properties: { width: 1920, height: 1080, fps: 30 },
60
+ tracks: [/* ... */]
61
+ }
62
+ });
63
+ };
64
+
65
+ return (
66
+ <div>
67
+ <button onClick={handleRender} disabled={isRendering}>
68
+ Render Video
69
+ </button>
70
+ {isRendering && <progress value={progress} max={1} />}
71
+ {videoBlob && <button onClick={download}>Download</button>}
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## Configuration
78
+
79
+ ### BrowserRenderConfig
80
+
81
+ ```typescript
82
+ {
83
+ projectFile?: Project; // Optional custom project (defaults to @twick/visualizer)
84
+ variables: {
85
+ input: any; // Required: project input data
86
+ playerId?: string;
87
+ [key: string]: any;
88
+ };
89
+ settings?: {
90
+ width?: number; // Video width (default: 1920)
91
+ height?: number; // Video height (default: 1080)
92
+ fps?: number; // Frames per second (default: 30)
93
+ quality?: 'low' | 'medium' | 'high';
94
+ range?: [number, number]; // [start, end] in seconds
95
+ onProgress?: (progress: number) => void;
96
+ onComplete?: (videoBlob: Blob) => void;
97
+ onError?: (error: Error) => void;
98
+ };
99
+ }
100
+ ```
101
+
102
+ ## Custom Project
103
+
104
+ ```typescript
105
+ import myProject from './my-project';
106
+
107
+ const videoBlob = await renderTwickVideoInBrowser({
108
+ projectFile: myProject, // Must be an imported Project object
109
+ variables: { input: {...} }
110
+ });
111
+ ```
112
+
113
+ **Note**: String paths only work in Node.js. In the browser, you must import and pass the Project object directly.
114
+
115
+ ## WASM Setup
116
+
117
+ Copy the WASM file to your public directory:
118
+
119
+ ```bash
120
+ cp node_modules/mp4-wasm/dist/mp4-wasm.wasm public/
121
+ ```
122
+
123
+ Or configure Vite to serve it:
124
+
125
+ ```typescript
126
+ // vite.config.ts
127
+ export default defineConfig({
128
+ assetsInclude: ['**/*.wasm']
129
+ });
130
+ ```
131
+
132
+ ## Limitations
133
+
134
+ - **Audio**: Audio processing is not yet implemented. Only video encoding is supported.
135
+ - **Browser Support**: Requires WebCodecs API (Chrome 94+, Edge 94+)
136
+
137
+ ## API Comparison
138
+
139
+ This package follows the same API as the server renderer (`@twick/renderer`):
140
+
141
+ | Feature | Server | Browser |
142
+ |---------|--------|---------|
143
+ | Duration Calculation | `renderer.getNumberOfFrames()` | ✅ Same |
144
+ | Playback State | `PlaybackState.Rendering` | ✅ Same |
145
+ | Frame Progression | `playback.progress()` | ✅ Same |
146
+ | Variables Assignment | `project.variables = {...}` | ✅ Same |
147
+ | Audio Support | ✅ FFmpeg | ❌ Not yet |
148
+
149
+ ## License
150
+
151
+ MIT
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@twick/browser-render",
3
+ "version": "0.15.6",
4
+ "license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
5
+ "description": "Browser-native video rendering for Twick using WebCodecs API",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "test:browser": "tsx src/test-browser-render.ts",
20
+ "clean": "rimraf dist"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@twick/core": "^0.15.6",
27
+ "@twick/visualizer": "0.15.6",
28
+ "mp4-wasm": "^1.0.6",
29
+ "mp4box": "^0.5.2",
30
+ "@ffmpeg/ffmpeg": "^0.12.10",
31
+ "@ffmpeg/util": "^0.12.1",
32
+ "@ffmpeg/core": "^0.12.6"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=17.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "react": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.10.0",
44
+ "@types/react": "^18.2.0",
45
+ "rimraf": "^5.0.5",
46
+ "tsup": "^8.0.0",
47
+ "tsx": "^4.7.0",
48
+ "typescript": "5.4.2"
49
+ },
50
+ "engines": {
51
+ "node": ">=20.0.0"
52
+ }
53
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@twick/browser-render",
3
+ "version": "0.15.6",
4
+ "license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
5
+ "description": "Browser-native video rendering for Twick using WebCodecs API",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "test:browser": "tsx src/test-browser-render.ts",
20
+ "clean": "rimraf dist"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@twick/core": "^0.15.6",
27
+ "@twick/visualizer": "0.15.6",
28
+ "mp4-wasm": "^1.0.6",
29
+ "mp4box": "^0.5.2",
30
+ "@ffmpeg/ffmpeg": "^0.12.10",
31
+ "@ffmpeg/util": "^0.12.1",
32
+ "@ffmpeg/core": "^0.12.6"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=17.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "react": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.10.0",
44
+ "@types/react": "^18.2.0",
45
+ "rimraf": "^5.0.5",
46
+ "tsup": "^8.0.0",
47
+ "tsx": "^4.7.0",
48
+ "typescript": "5.4.2"
49
+ },
50
+ "engines": {
51
+ "node": ">=20.0.0"
52
+ }
53
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Service Worker for audio processing
3
+ * Handles audio extraction and processing in the background
4
+ */
5
+
6
+ const CACHE_NAME = 'twick-audio-cache-v1';
7
+ const AUDIO_CACHE = 'audio-assets';
8
+
9
+ self.addEventListener('install', (event) => {
10
+ console.log('Audio Service Worker installed');
11
+ self.skipWaiting();
12
+ });
13
+
14
+ self.addEventListener('activate', (event) => {
15
+ console.log('Audio Service Worker activated');
16
+ event.waitUntil(self.clients.claim());
17
+ });
18
+
19
+ /**
20
+ * Intercept audio/video requests for processing
21
+ */
22
+ self.addEventListener('fetch', (event) => {
23
+ const url = new URL(event.request.url);
24
+
25
+ // Check if this is a media request we should cache
26
+ const isMedia = /\.(mp4|webm|mp3|wav|ogg|m4a)$/i.test(url.pathname);
27
+
28
+ if (isMedia) {
29
+ event.respondWith(
30
+ caches.open(AUDIO_CACHE).then((cache) => {
31
+ return cache.match(event.request).then((response) => {
32
+ if (response) {
33
+ return response;
34
+ }
35
+
36
+ return fetch(event.request).then((networkResponse) => {
37
+ // Cache for future use
38
+ cache.put(event.request, networkResponse.clone());
39
+ return networkResponse;
40
+ });
41
+ });
42
+ })
43
+ );
44
+ }
45
+ });
46
+
47
+ /**
48
+ * Handle messages from main thread
49
+ */
50
+ self.addEventListener('message', async (event) => {
51
+ const { type, data } = event.data;
52
+
53
+ switch (type) {
54
+ case 'EXTRACT_AUDIO':
55
+ // Extract audio from video URL
56
+ const audioData = await extractAudio(data.url);
57
+ event.ports[0].postMessage({ type: 'AUDIO_EXTRACTED', data: audioData });
58
+ break;
59
+
60
+ case 'CLEAR_CACHE':
61
+ await caches.delete(AUDIO_CACHE);
62
+ event.ports[0].postMessage({ type: 'CACHE_CLEARED' });
63
+ break;
64
+ }
65
+ });
66
+
67
+ /**
68
+ * Extract audio from video URL
69
+ */
70
+ async function extractAudio(url) {
71
+ try {
72
+ const response = await fetch(url);
73
+ const blob = await response.blob();
74
+
75
+ // Return blob for processing in main thread
76
+ // (Web Audio API is not available in service workers)
77
+ return {
78
+ url,
79
+ blob: await blobToArrayBuffer(blob),
80
+ size: blob.size
81
+ };
82
+ } catch (error) {
83
+ console.error('Failed to extract audio:', error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ async function blobToArrayBuffer(blob) {
89
+ return new Promise((resolve, reject) => {
90
+ const reader = new FileReader();
91
+ reader.onload = () => resolve(reader.result);
92
+ reader.onerror = reject;
93
+ reader.readAsArrayBuffer(blob);
94
+ });
95
+ }