@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.
- package/AUDIO_IMPLEMENTATION.md +217 -0
- package/README.md +151 -0
- package/package.json +53 -0
- package/package.json.bak +53 -0
- package/public/audio-worker.js +95 -0
- package/src/audio/audio-processor.ts +239 -0
- package/src/audio/audio-video-muxer.ts +79 -0
- package/src/browser-renderer.ts +495 -0
- package/src/hooks/use-browser-renderer.ts +218 -0
- package/src/index.ts +20 -0
- package/src/mp4-wasm.d.ts +4 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +19 -0
|
@@ -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
|
+
}
|
package/package.json.bak
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,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
|
+
}
|