Skip to content

Commit 4c02a41

Browse files
authored
Media recorder support (#511)
* feat: add MediaRecorder migration specification and outline new export workflows * feat: implement RecordingService for video and audio processing * feat: refactor VideoService to delegate audio and video conversion to RecordingService * feat: add gifenc package * feat: refactor VideoService to implement GIF encoding and improve error handling * feat: migrate from VideoService to MediaExportService and remove FFmpeg dependencies * feat: add setup script for Chromium installation for unit tests * feat: refactor Animator and MediaExportService to streamline video creation and remove WebMWriter dependency * chore: Remove FFmpeg WebAssembly core and worker script files to streamline the project and eliminate unused dependencies. * fix: update comment in formatProgress method for clarity on progress capping * refactor: enhance documentation in AnimatorService and MediaExportService for clarity and maintainability * chore: remove unused .vscode entry from .gitignore and update mediarecorder migration spec to English * feat: add production Docker Compose configuration for app service * chore: remove tentative timeline section from mediarecorder migration spec * refactor: improve file loading process and enhance error handling in Animator class * feat: implement MediaImportService for handling media file imports and decoding * chore: update packageManager version in package.json * test: add unit tests for MediaImportService functionality * feat: enhance MediaImportService to return video and audio blobs, refactor load method in Animator class * refactor: update load method signatures in Animator and AnimatorService to use Blob type * fix: improve error handling and refactor decodeFile method in Animator class * fix: update handleFrame to accept blob and index parameters in Animator class * feat: add ADR 0001 to enforce VP8 WebM exports for improved compatibility * feat: implement frame manifest support in media import and animator services
1 parent d66122d commit 4c02a41

31 files changed

Lines changed: 1386 additions & 1026 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ npm-debug.log*
2222
/.sass-cache
2323
/.sourcemaps
2424
/.versions
25-
/.vscode
2625
/coverage
2726
/dist
2827
/node_modules

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,18 @@ yarn test
4242

4343
- [Codec Documentation](docs/CODECS.md) - Information about image, video, and audio codecs used
4444
- [Third-Party Licenses](THIRD_PARTY_LICENSES.md) - License information for codec-related dependencies
45+
46+
## Chromium setup for unit tests
47+
48+
Karma runs the Angular unit tests inside ChromiumHeadless. If the container or host image does not already provide Chromium, run:
49+
50+
```
51+
./scripts/setup-chromium.sh
52+
```
53+
54+
After installation export `CHROMIUM_BIN` so Karma picks up the binary (add this to your shell profile if you run tests frequently):
55+
56+
```
57+
export CHROMIUM_BIN=$(which chromium)
58+
npm run test
59+
```

THIRD_PARTY_LICENSES.md

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@ This document lists third-party libraries and their licenses used in the StopCli
44

55
## Codec-Related Dependencies
66

7-
### FFmpeg.wasm (@ffmpeg/ffmpeg)
7+
### gifenc
88

9-
**Version:** 0.12.15
9+
**Version:** 1.0.3
1010
**License:** MIT License
11-
**Purpose:** Used for video and audio encoding/decoding, format conversion
12-
**Source:** https://github.com/ffmpegwasm/ffmpeg.wasm
13-
14-
FFmpeg.wasm is a pure WebAssembly port of FFmpeg. It enables video & audio record, convert and stream right inside browsers.
11+
**Purpose:** Generates GIF animations client-side during export
12+
**Source:** https://github.com/mattdesl/gifenc
13+
**Author:** Matt DesLauriers
1514

1615
**License Text:**
1716

1817
```
1918
MIT License
2019
21-
Copyright (c) 2019 Jerome Wu
22-
2320
Permission is hereby granted, free of charge, to any person obtaining a copy
2421
of this software and associated documentation files (the "Software"), to deal
2522
in the Software without restriction, including without limitation the rights
@@ -39,34 +36,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3936
SOFTWARE.
4037
```
4138

42-
**Note:** FFmpeg.wasm uses libraries from the FFmpeg project under the LGPLv2.1. See https://ffmpeg.org for more information.
43-
44-
### webm-writer
45-
46-
**Version:** 1.0.0
47-
**License:** WTFPL (Do What The Fuck You Want To Public License)
48-
**Purpose:** Creates WebM videos from Canvas frames for draft saves
49-
**Source:** https://github.com/thenickdude/webm-writer-js
50-
**Author:** Nicholas Sherlock
51-
52-
**License Text:**
53-
54-
```
55-
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
56-
Version 2, December 2004
57-
58-
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
59-
60-
Everyone is permitted to copy and distribute verbatim or modified
61-
copies of this license document, and changing it is allowed as long
62-
as the name is changed.
63-
64-
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
65-
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
66-
67-
0. You just DO WHAT THE FUCK YOU WANT TO.
68-
```
69-
7039
### webm.js
7140

7241
**License:** BSD Zero Clause License (BSD-0)
@@ -105,7 +74,7 @@ The application uses the following codecs:
10574

10675
### Video Formats
10776
- **WebM:** Container format
108-
- **VP8:** Video codec (via libvpx in FFmpeg)
77+
- **VP8:** Video codec (via the browser's MediaRecorder implementation)
10978
- **VP9:** Not currently used but supported by modern browsers
11079

11180
### Audio Formats
@@ -125,7 +94,7 @@ All third-party licenses are compatible with the GNU Affero General Public Licen
12594
- **MIT License:** Permissive, GPL-compatible
12695
- **WTFPL:** Public domain equivalent, GPL-compatible
12796
- **BSD-0:** Permissive, GPL-compatible
128-
- **LGPLv2.1 (FFmpeg):** Compatible with AGPL through dynamic linking
97+
- **MIT License (gifenc):** Permissive and GPL-compatible
12998

13099
## Additional Dependencies
131100

docker-compose.prod.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
app:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
target: production
7+
ports:
8+
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-8080}:8080"

docs/CODECS.md

Lines changed: 20 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,7 @@ this.imageCanvas.toBlob(async (blob: Blob) => {
2626

2727
### Internal Storage: WebP
2828

29-
All frames are converted to **WebP** format for internal processing:
30-
31-
```typescript
32-
await this.ffmpeg.exec([
33-
"-i", inputPath,
34-
"-c:v", "libwebp",
35-
"-lossless", "0",
36-
"-compression_level", "4",
37-
"-quality", "65",
38-
outputPath
39-
]);
40-
```
29+
All frames are converted to **WebP** format for internal processing using the browser's Canvas APIs. Frames that arrive as JPEG are decoded via `createImageBitmap`, rendered into an offscreen canvas, and saved back as WebP blobs for consistent downstream handling.
4130

4231
**Rationale:**
4332
- Better compression than JPEG (25-35% smaller files)
@@ -50,7 +39,7 @@ await this.ffmpeg.exec([
5039
The application handles mixed JPEG/WebP frames intelligently:
5140

5241
1. **Capture:** Frames are captured as JPEG from canvas
53-
2. **Storage:** JPEG frames are converted to WebP via FFmpeg for consistency
42+
2. **Storage:** JPEG frames are converted to WebP via Canvas for consistency
5443
3. **Export:** All frames are WebP when creating videos or GIFs
5544
4. **Import:** Loaded frames are stored as WebP internally
5645

@@ -69,7 +58,7 @@ This approach ensures:
6958
All video outputs use the **WebM** container format:
7059

7160
```typescript
72-
const result = await this.videoService.createVideo(
61+
const result = await this.mediaExportService.createVideo(
7362
this.animator.frameWebpsAndJpegs,
7463
frameRate,
7564
this.animator.audioBlob,
@@ -84,27 +73,18 @@ const result = await this.videoService.createVideo(
8473
- Native HTML5 video element support
8574
- Good compression ratios
8675

87-
### Video Codec: VP8 (libvpx)
76+
### Video Codec: VP8 (MediaRecorder)
8877

89-
Video encoding uses **VP8** codec via FFmpeg:
90-
91-
```typescript
92-
parameters.push(
93-
"-vcodec", "libvpx",
94-
"-vf", "scale=640:-2,format=yuv420p",
95-
outputFileName
96-
);
97-
```
78+
Video encoding uses **VP8** via the browser's native MediaRecorder implementation. The recording pipeline renders frames onto an offscreen canvas, captures a `MediaStream` with `canvas.captureStream(frameRate)`, optionally merges an audio stream, and records with a VP8-capable MIME type (preferring `video/webm;codecs=vp9,opus` and falling back to `video/webm;codecs=vp8,opus`).
9879

9980
**Rationale:**
100-
- Royalty-free
101-
- Broad browser support (Chrome 6+, Firefox 4+, Safari 14.1+, Edge 79+)
102-
- Good balance of quality and file size
103-
- Hardware acceleration available on most devices
81+
- Royalty-free and hardware-accelerated in modern browsers
82+
- Eliminates heavy WASM workers and virtual file systems
83+
- Seamlessly combines audio/video tracks via `MediaStream`
10484

10585
**Video Processing Options:**
106-
- Scale to max width of 640px (maintains aspect ratio)
107-
- YUV 4:2:0 color space (standard for web video)
86+
- Scale to max width of 640px before rendering
87+
- YUV 4:2:0 color space is enforced by the browser implementation
10888
- Configurable frame rate (default: 6 fps for stop motion)
10989

11090
### Future Consideration: VP9
@@ -137,38 +117,15 @@ this.audioRecorder = new MediaRecorder(this.audioStream, {
137117

138118
### Storage: WebM/Opus
139119

140-
Regardless of the recorder outcome, audio blobs are converted to **WebM/Opus**:
141-
142-
```typescript
143-
await this.ffmpeg.exec([
144-
"-i", inputAudioPath,
145-
"-vn",
146-
"-c:a", "libopus",
147-
outputPath // .webm
148-
]);
149-
```
150-
151-
**Rationale:**
152-
- Aligns with the WebM-based video pipeline
153-
- Opus is royalty-free and optimised for voice
120+
Regardless of the recorder outcome, audio blobs are normalized to **WebM/Opus** via an `AudioContext`. The export service decodes the blob, replays it through a `MediaStreamAudioDestination`, and records the result so downstream consumers always receive an Opus/WebM payload.
154121

155122
## GIF Export
156123

157-
GIF creation uses FFmpeg with a two-pass palette approach:
158-
159-
```typescript
160-
const gifParameters = [
161-
"-r", `${frameRate}`,
162-
"-i", inputPattern,
163-
"-vf", `fps=${frameRate},scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
164-
"-loop", "0",
165-
outputFileName
166-
];
167-
```
124+
GIF creation uses the lightweight **gifenc** library. Frames are decoded, resized to a maximum width of 480px, quantized to an indexed palette, and encoded via gifenc's `GifEncoder`. Progress callbacks fire between batches so the UI can stay responsive.
168125

169126
**Features:**
170-
- Palette generation for optimal colors
171-
- Lanczos scaling for quality
127+
- Deterministic palette building and dithering for good quality
128+
- Runs entirely on the client without WASM payloads
172129
- Scaled to 480px width for reasonable file size
173130
- Infinite loop by default
174131

@@ -186,26 +143,9 @@ project.zip
186143

187144
**Implementation:**
188145
- Uses @zip.js/zip.js library
189-
- Frames are encoded to WebM using webm-writer library
146+
- Video blobs originate from `MediaRecorder` (via MediaExportService)
190147
- Audio is stored separately if present
191148

192-
### WebM Draft Format
193-
194-
Draft videos are created using the webm-writer library:
195-
196-
```typescript
197-
const videoWriter = new WebMWriter({
198-
quality: 0.95,
199-
frameRate: frameRate,
200-
transparent: false
201-
});
202-
```
203-
204-
**Rationale:**
205-
- Fast encoding in browser
206-
- Compatible with our import pipeline
207-
- No server-side processing required
208-
209149
## Browser Compatibility Matrix
210150

211151
| Format | Chrome | Firefox | Safari | Edge |
@@ -238,7 +178,7 @@ The consistent use of WebM and WebP ensures:
238178
### Memory Management
239179

240180
1. **Batch Processing:** JPEG→WebP conversion is batched to prevent memory issues
241-
2. **Streaming:** FFmpeg processes frames in a temporary directory
181+
2. **Streaming:** MediaRecorder writes directly to in-memory blobs
242182
3. **Cleanup:** Working directories are cleaned up after processing
243183

244184
### Processing Time
@@ -256,15 +196,10 @@ Times vary based on:
256196

257197
## Dependencies
258198

259-
### FFmpeg.wasm
260-
- **Purpose:** Video/audio encoding, format conversion
261-
- **License:** MIT (with LGPL FFmpeg libraries)
262-
- **Version:** 0.12.15
263-
264-
### webm-writer
265-
- **Purpose:** Draft video creation
266-
- **License:** WTFPL
267-
- **Version:** 1.0.0
199+
### gifenc
200+
- **Purpose:** GIF encoding
201+
- **License:** MIT
202+
- **Version:** 1.0.3
268203

269204
### webm.js
270205
- **Purpose:** WebM container decoding
@@ -287,5 +222,4 @@ Times vary based on:
287222
## Related Documentation
288223

289224
- [Third-Party Licenses](../THIRD_PARTY_LICENSES.md)
290-
- [FFmpeg.wasm Documentation](https://ffmpegwasm.netlify.app/)
291225
- [WebM Container Specification](https://www.webmproject.org/docs/container/)

0 commit comments

Comments
 (0)