# Cap H.264 production encoding baselines This file lists the exact H.264 encoder settings used by each Cap surface, so a benchmark harness can reproduce them on Linux. ## Sources and environment | Item | Value | |---|---| | Cap repo | `/workspace` on branch `main`, commit `d327051c1` (read only) | | PR 2312 | `FETCH_HEAD` = `549c5afa9` (`origin/feature/web-editor-desktop-parity`), read with `git show FETCH_HEAD:` | | Rust ffmpeg bindings | `ffmpeg-next`, CapSoftware fork rev `49db1fede112` | | Verification ffmpeg | 6.1.1-3ubuntu5 | | Verification libx264 | core 164 r3108 | | Verification Chrome | Google Chrome 148.0.7778.96 at `/usr/local/bin/google-chrome`, headless | | Verification CPU | `nproc` = 4 | Line numbers refer to `main` unless marked "PR". A value marked "verified" was observed in an encode on this VM, either in ffprobe output or in the x264 SEI options string. A value marked "source" was read from code but not run. Hardware encoders (VideoToolbox, NVENC, QSV, AMF, Media Foundation, AVFoundation) cannot run on this VM. ## Contents 0. Conventions and shared facts 1. Media server 2. Web app server-side ffmpeg 3. Render farm 4. Desktop recording 5. Desktop export 6. Web recorders on `main` (MediaRecorder) 7. Browser WebCodecs encoders on `main` 8. PR 2312 differences 9. Running browser surfaces in headless Chrome with Playwright 10. Ambiguities and assumptions 11. Summary table --- ## 0. Conventions and shared facts ### 0.1 Placeholders | Placeholder | Meaning | |---|---| | `W`, `H` | Output width and height | | `FPS` | Frame rate | | `B` | Target bitrate in bit/s | | `M` | maxrate, always `B * 3 / 2` with integer division where Cap sets it | | `K` | Keyframe interval in frames | | `N` | `nproc` | All ffmpeg reproductions read raw input with: ``` -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv ``` ### 0.2 Shared bits-per-pixel (bpp) bitrate model Desktop export, desktop recording through `enc-ffmpeg`, the render farm, and the PR browser export all use this model. | Implementation | Location | |---|---| | `get_bitrate` | `crates/enc-ffmpeg/src/video/h264.rs` lines 1651-1658 | | `bitrate` | `crates/render-farm/src/video.rs` lines 184-187 | | `exportBitrate` (PR) | `packages/editor-solid-web/src/browser-export-estimate.ts` | The formula is: ``` B = W * H * ((max(FPS - 30, 0) * 0.6) + 30) * bpp M = B * 3 / 2 (Rust only: encoder.set_max_bit_rate(bitrate * 3 / 2)) ``` How each implementation computes it: - **Rust:** bpp is an `f32`, widened to `f64`, and the result is truncated to `usize`. That gives slightly odd values, for example 29,859,841. - **PR browser export:** uses JavaScript `f64` with `Math.round`. Worked values for the Rust model, shown as `B (M)`: | bpp | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | 1.0 | 27,648,000 (41,472,000) | 62,208,000 (93,312,000) | 99,532,800 (149,299,200) | 248,832,000 (373,248,000) | | 0.3 | 8,294,400 (12,441,600) | 18,662,400 (27,993,600) | 29,859,841 (44,789,761) | 74,649,602 (111,974,403) | | 0.18 | 4,976,640 (7,464,960) | 11,197,440 (16,796,160) | 17,915,904 (26,873,856) | 44,789,761 (67,184,641) | | 0.15 | 4,147,200 (6,220,800) | 9,331,200 (13,996,800) | 14,929,920 (22,394,880) | 37,324,801 (55,987,201) | | 0.08 | 2,211,839 (3,317,758) | 4,976,639 (7,464,958) | 7,962,623 (11,943,934) | 19,906,559 (29,859,838) | | 0.04 | 1,105,919 (1,658,878) | 2,488,319 (3,732,478) | 3,981,311 (5,971,966) | 9,953,279 (14,929,918) | Browser model (`Math.round`, `f64`), B only: | bpp | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | 0.3 | 8,294,400 | 18,662,400 | 29,859,840 | 74,649,600 | | 0.15 | 4,147,200 | 9,331,200 | 14,929,920 | 37,324,800 | | 0.08 | 2,211,840 | 4,976,640 | 7,962,624 | 19,906,560 | | 0.04 | 1,105,920 | 2,488,320 | 3,981,312 | 9,953,280 | ### 0.3 Shared keyframe interval `crates/enc-ffmpeg/src/video/h264.rs` lines 1223-1239: ``` DEFAULT_KEYFRAME_INTERVAL_SECS = 2 K = round(2 * FPS).max(1) ``` | FPS | K | |---|---| | 30 | 60 | | 60 | 120 | The render farm uses `fps * 2` with integer fps (`crates/render-farm/src/video.rs` line 473; keyframe secs = 2 at line 629). That gives the same values. ### 0.4 How Cap's x264 options reach libx264 (applies to every `enc-ffmpeg` and render-farm libx264 path) Options are passed as an AVDictionary to `open_as_with`, which calls `avcodec_open2`. Keys that libx264/AVCodecContext does not recognise are silently left unconsumed and ignored. | Key | Status | |---|---| | `preset`, `tune`, `crf`, `threads`, `bf`, `rc-lookahead`, `aq-mode`, `trellis`, `g`, `keyint_min` | Consumed | | `b-adapt` | Ignored. The real option name is `b_strategy`. | | `ref` | Ignored. The real option name is `refs`. | | `subme` | Ignored. The real option name is `subq`. | | `pix_fmt` | Ignored. It is not a codec option. | Effect, verified from the x264 SEI string: - The export/render-farm "fast" set produces `ref=1 subme=2`, which are the veryfast preset defaults. The intended `ref 2` never applies. - `subme 2` and `trellis 0` happen to equal the veryfast defaults. - `b-adapt` is irrelevant because `bf 0`. Two more x264 behaviours, both verified: - **`maxrate` is ignored.** Cap sets `rc_max_rate` (`M`) but never sets `rc_buffer_size`. x264 logs `VBV maxrate specified, but no bufsize, ignored`, so every Cap libx264 bitrate encode is plain ABR (`rc=abr`, no VBV). - **`keyint_min` is clamped.** Cap sets `keyint_min = K`. x264 clamps it to `K/2 + 1`, so 60 becomes 31 and 120 becomes 61. ### 0.5 Shared `enc-ffmpeg` encoder context Source: `crates/enc-ffmpeg/src/video/h264.rs` lines 775-816. **Threading:** `threads = available_parallelism()` via `set_threading(Config::count(...))`. **Colour tags:** - colorspace BT709 - color_range MPEG (limited) - color_primaries BT709 - color_trc BT709 **Rate control:** - CRF mode: `bit_rate = 0`. - Otherwise: `bit_rate = get_bitrate(out_w, out_h, fps, bpp)` and `max_bit_rate = bit_rate * 3 / 2`. **Timing:** - Encoder time base is the input `VideoInfo.time_base`. - Muxer stream time base is `1/90000` (lines 286 and 413). **Pixel format (lines 671-688):** - Uses the input format if the encoder supports it. - Otherwise YUV420P for libx264 and NV12 for the others. **Scaler (lines 627-657):** - BICUBIC when resizing, FAST_BILINEAR otherwise. - RGB input uses BT.709 coefficients, full-range in, limited-range out. **Encoder priority:** | Platform | Order | |---|---| | macOS | `h264_videotoolbox`, `libx264` | | Windows, NVIDIA GPU | `h264_nvenc`, `h264_mf`, `h264_qsv`, `h264_amf`, `libx264` | | Windows, AMD GPU | `h264_amf`, `h264_mf`, `h264_nvenc`, `h264_qsv`, `libx264` | | Windows, Intel GPU | `h264_qsv`, `h264_mf`, `h264_nvenc`, `h264_amf`, `libx264` | | Windows, other or unknown GPU | `h264_nvenc`, `h264_qsv`, `h264_amf`, `h264_mf`, `libx264` | | Windows, GPU without hardware encoding | `libx264` | | Linux with `/dev/nvidiactl` present | `h264_nvenc`, `libx264` | | Linux otherwise | `libx264` | Sources: `get_default_encoder_priority` at lines 1133-1173 and `linux_encoder_priority` at lines 1124-1131. **Priority overrides:** - `CAP_EXPORT_FORCE_SOFTWARE_ENCODER` set to `1`, `true`, `yes` or `on` forces `libx264` (line 1192). - `requires_software_encoder` (line 1062) never applies to exports. For recordings it forces `libx264` when: - the preset is `HighThroughput`, or - on macOS/Windows, FPS is above 0.9 times the estimated hardware maximum. - A CRF request always forces `libx264` (line 1241). - Windows export with an AMD GPU uses `h264_amf`, `h264_mf`, `h264_nvenc`, `h264_qsv`, `libx264` (lines 1201-1221). **Fallback:** encoders are tried in priority order, and the first that opens is used. ### 0.6 Option sets per encoder Source: `crates/enc-ffmpeg/src/video/h264.rs` lines 1258-1357, quoted literally. **`h264_videotoolbox`** | Mode | Options | |---|---| | Export | `realtime=false`, `profile=main`, `allow_sw=0` | | Recording | `realtime=true`, `prio_speed=true`, `profile=main` | No `g` is set in either mode. **`h264_nvenc`** | Mode | Options | |---|---| | Export | `preset=p5`, `tune=hq`, `rc=vbr`, `spatial-aq=1`, `temporal-aq=1`, `b_ref_mode=middle`, `g=K` | | Recording | `preset=p4`, `tune=ll`, `rc=vbr`, `spatial-aq=1`, `temporal-aq=1`, `g=K` | **`h264_qsv`** | Mode | Options | |---|---| | Export | `preset=medium`, `look_ahead=1`, `look_ahead_depth=20`, `g=K` | | Recording | `preset=faster`, `look_ahead=1`, `g=K` | **`h264_amf`** | Mode | Options | |---|---| | Export | `quality=quality`, `rc=vbr_peak`, `g=K` | | Recording | `quality=balanced`, `rc=vbr_latency`, `g=K` | **`h264_mf`** | Mode | Options | |---|---| | Export | `hw_encoding=true`, `scenario=0`, `quality=0`, `g=K` | | Recording | `hw_encoding=true`, `scenario=4`, `quality=1`, `g=K` | **`libx264`**, which branch applies: | Condition | Options | |---|---| | CRF requested | `preset=slow`, `crf=`, `pix_fmt=yuv420p` (ignored) | | Export, preset Slow | `preset=slow` | | Export, preset Medium | `preset=medium` | | Export, any other preset | `preset=veryfast`, `threads=N`, `bf=0`, `rc-lookahead=10`, `b-adapt=0` (ignored), `aq-mode=1`, `ref=2` (ignored), `subme=2` (ignored), `trellis=0` | | Recording, Slow or Medium | `preset=veryfast`, `tune=zerolatency` | | Recording, Ultrafast or HighThroughput | `preset=ultrafast`, `tune=zerolatency` | All `libx264` branches also set `g=K` and `keyint_min=K`. ### 0.7 VideoToolbox and NVENC defaults that Cap inherits (source: FFmpeg 7.1) **VideoToolbox (`libavcodec/videotoolboxenc.c`):** - `vt_defaults` only sets `b`, `qmin` and `qmax`, so `gop_size` keeps the AVCodecContext default of 12. - Because Cap never sets `g` for VideoToolbox, `MaxKeyFrameInterval = 12` frames. This is not 2 s. - `max_b_frames` defaults to 0, so `AllowFrameReordering = false` (no B-frames). - `profile=main` maps to `kVTProfileLevel_H264_Main_AutoLevel`. - `rc_max_rate` maps to `kVTCompressionPropertyKey_DataRateLimits` as `[M/8 bytes, 1 s]`. **NVENC:** - `g` is set by Cap. - `bf` stays at -1, meaning the preset decides. - `refs` stays at 0 (default). - `rc-lookahead` and `bufsize` are not set by Cap. ### 0.8 Audio summary | Surface | Audio | |---|---| | Media server processVideo | AAC 128k | | Media server edit | AAC 160k | | Render farm | AAC 320 kbps, 48 kHz | | Desktop `enc-ffmpeg` | AAC 320 kbps | The desktop value comes from `crates/enc-ffmpeg/src/audio/aac.rs` line 33: `OUTPUT_BITRATE: usize = 320 * 1000; // 128k`. The trailing comment is stale. --- ## 1. Media server (`apps/media-server`) Image: `apps/media-server/Dockerfile` uses `FROM oven/bun:1.4.0` plus apt `ffmpeg`, with no version pinned. ### 1.1 MS-TRANSCODE: `processVideo` video transcode **Location:** `apps/media-server/src/lib/media-video.ts`. | Lines | What | |---|---| | 162-172 | `DEFAULT_OPTIONS` | | 47-50 | Level size constants | | 1288-1302 | `needsVideoTranscode` | | 1304-1306 | `needsAudioTranscode` | | 1405-1420 | `pickMobileSafeH264Level` | | 1483-1581 | `processVideo` | **Constants resolved from `DEFAULT_OPTIONS`:** | Option | Value | |---|---| | maxWidth | 1920 | | maxHeight | 1080 | | crf | 23 | | preset | `"medium"` | | audioBitrate | `"128k"` | | videoBitrate | `"5M"` (defined, never used in any argument) | | remuxOnly | false | | normalizeH264Level | false | **Literal argument vector when `videoTranscode` is true:** ``` ffmpeg -threads 2 [extraInputArgs] -i -c:v libx264 -preset medium -crf 23 -vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2" -pix_fmt yuv420p -level:v <4.2|5.1|5.2> [-bf 0] # only when source codec is vp8 or vp9 <-c:a aac -b:a 128k | -c:a copy | -an> -movflags +faststart [extraOutputArgs] -progress pipe:2 -y .mp4 ``` **When it runs.** `needsVideoTranscode` is true when any of these hold: - source width > maxWidth, or height > maxHeight - the codec is not `h264` - the probed source H.264 level is above the target level **Level choice** (`pickMobileSafeH264Level`) uses the target size `(min(srcW, maxW), min(srcH, maxH))`: | Target size | Level | |---|---| | ≤2048x1088 | `4.2` | | ≤4096x2304 | `5.1` | | Larger | `5.2` | With the defaults every transcode is level 4.2. **Callers:** | Caller | Options passed | Effect | |---|---|---| | Web workflows (`process-video.ts`, admin reprocess) | No size or quality options | Defaults apply: a 1080p cap, level 4.2 | | `/video/convert` (`routes/video.ts` line 594) | Source dimensions as max | No downscale; 4K gets level 5.1 | | Multipart (`multipart.ts` line 977) | `remuxOnly: true` for web MP4 | Video and audio copied | **Audio:** - Source audio is AAC: `-c:a copy`. - Other source audio: `-c:a aac -b:a 128k`. - No audio: `-an`. **Resilient mode.** The retry path adds these flags, built at `routes/video.ts` lines 842-847: | Position | Flags | |---|---| | Input | `-err_detect ignore_err -fflags +genpts+discardcorrupt` | | Output | `-max_muxing_queue_size 1024` | **Encoder settings:** | Setting | Value | |---|---| | Encoder | libx264 | | Rate control | CRF 23. No bitrate cap or VBV. | | GOP | x264 defaults: keyint 250, keyint_min 25, scenecut 40 (verified) | | B-frames, VP8/VP9 source | `-bf 0` (verified bframes=0) | | B-frames, other sources | x264 medium default bframes=3 | | Preset / tune | `medium`, no tune | | x264 internals at medium | ref=3, subme=7, trellis=1, rc_lookahead=40 (verified) | | Profile | High (x264 automatic, verified) | | Level | Forced by `-level:v` (verified 42) | | Pixel format | yuv420p | **Threads.** `-threads 2` is placed before `-i`, so it applies to the decoder only. The encoder uses ffmpeg's automatic count: verified threads=6 on 4 cores. **Colour.** No colour options are set, so tags are passed through from the decoder. Untagged sources stay untagged. **Scaling:** - First: fit inside 1920x1080, preserving aspect ratio, never upscaling. - Then: round both dimensions down to even. - Scaler flags: swscale default (bicubic). **Frame rate:** - No `-r`, no fps filter, no `-fps_mode`/`-vsync`. - The ffmpeg CLI `fps_mode auto` picks CFR for the MP4 muxer, so VFR browser captures are duplicated/dropped to the rate ffmpeg guesses for the input. - Verified: a VFR WebM tagged 30/1 came out as 90 frames at 30/1. **Container:** MP4 with `+faststart`. Audio, when transcoded, is AAC 128k. **Reproduction on Linux (verified):** ``` ffmpeg -y -threads 2 -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -c:v libx264 -preset medium -crf 23 \ -vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2" \ -pix_fmt yuv420p -level:v 4.2 -an -movflags +faststart ms_transcode.mp4 ``` - **MS-TRANSCODE-VPX:** add `-bf 0` before `-an`. This is the variant that applies to Chrome WebM recordings on `main`. - **3840x2160 input:** with default options, the output is 1920x1080 at level 4.2. ### 1.2 MS-LEVELFIX: level rewrite without re-encode (dormant) **Location:** `media-video.ts` lines 1533-1539. ``` ffmpeg -threads 2 -i -c:v copy -bsf:v h264_metadata=level=<4.2|5.1|5.2> ... -movflags +faststart -progress pipe:2 -y ``` It runs only when `opts.normalizeH264Level` is true and the source fits inside the size limits with a level above the target. No caller sets `normalizeH264Level`, so this is not reached in production. A too-high level instead causes a full MS-TRANSCODE. ### 1.3 Other media-server ffmpeg invocations (no H.264 encode) **Copy path:** `-c:v copy` in `processVideo`, used for H.264 sources within the size and level limits. **Container repair** (`media-video.ts` lines 1325-1361): ``` ffmpeg -threads 2 -err_detect ignore_err -fflags +genpts+igndts -i -c copy -y .mkv ``` **Edit concat** (`media-edit.ts` `buildConcatArgs`, line 207): ``` ffmpeg -hide_banner -y -f concat -safe 0 -i -map 0 -c copy -movflags +faststart ``` **Unused:** `buildTranscodeSegmentArgs` and `buildStreamCopySegmentArgs` in `media-edit.ts` have no callers. ### 1.4 MS-EDIT: edited-video render (`renderEditedVideo`) **Location:** `apps/media-server/src/lib/media-edit.ts`. | Lines | What | |---|---| | 83-87 | `getOutputFps` | | 151-205 | `buildTranscodeEditArgs` | | 446-469 | `renderEditedVideo` | It is called from `routes/video.ts` line 1168. **Inputs and constants:** - `fps` is the probed `metadata.fps`. - `getOutputFps(fps) = min(120, max(1, round(fps*100)/100))`, or `DEFAULT_OUTPUT_FPS` = 30 when fps is missing or invalid. - Up to `MAX_TRANSCODE_RANGES_PER_BATCH` = 4 ranges per ffmpeg run. Multiple batches are joined by the concat copy in section 1.3. **Literal arguments:** ``` ffmpeg -hide_banner -y (-ss -t -i ) x ranges -filter_complex "[i:v:0]fps=F,setpts=PTS-STARTPTS[vi];[i:a:0]asetpts=PTS-STARTPTS[ai];...;[v0][a0]...concat=n=R:v=1:a=1[v][a]" -map [v] -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -enc_time_base:v 1/F (-map [a] -c:a aac -b:a 160k | -an) -movflags +faststart ``` Without audio, each video filter is `[i:v:0]fps=F,setpts=PTS-STARTPTS[vi]` and the concat is `concat=n=R:v=1:a=0[v]`. **Encoder settings:** | Setting | Value | |---|---| | Encoder | libx264 | | Rate control | CRF 18 | | GOP | x264 default keyint 250, min 25 | | B-frames | x264 fast default bframes=3, b_adapt=1 (verified has_b_frames=2) | | Preset / tune | `fast`, no tune | | x264 internals at fast | ref=2, subme=6, rc_lookahead=30 (verified) | | Profile | High (verified) | | Level | x264 automatic (verified 3.1 at 720p30) | | Pixel format | yuv420p | | Colour | Passed through from input | **Frame rate:** CFR at `F`, enforced by the `fps` filter, with `-enc_time_base:v 1/F`. **Container and audio:** MP4 with `+faststart`; AAC 160k. **Reproduction on Linux** (single range, no audio, exactly the filter string the code builds; verified): ``` ffmpeg -hide_banner -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -filter_complex "[0:v:0]fps=FPS,setpts=PTS-STARTPTS[v0];[v0]concat=n=1:v=1:a=0[v]" \ -map "[v]" -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -enc_time_base:v 1/FPS \ -an -movflags +faststart ms_edit.mp4 ``` --- ## 2. Web app server-side ffmpeg ### 2.1 WEB-LOOM-FFMPEG: Loom download conversion **Location:** `apps/web/lib/video-convert.ts`, used by `app/api/tools/loom-download/route.ts`. It first tries a remux (line 212): ``` ffmpeg -y -i -c copy -movflags +faststart ``` If the remux fails, it re-encodes (line 223): ``` ffmpeg -y -i -c:v libx264 -preset veryfast -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart ``` **Encoder settings:** | Setting | Value | |---|---| | Rate control | CRF 23 (libx264 default, no `-crf` given) | | GOP | 250/25 | | B-frames | veryfast default bframes=3 (verified has_b_frames=2) | | Profile / level | High, automatic level (verified 3.1 at 720p30) | | Frame rate | CLI auto, so CFR for MP4 | | Colour | Passed through | | Container | MP4 `+faststart` | | Audio | AAC 128k | **Reproduction on Linux (verified):** ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -c:v libx264 -preset veryfast -pix_fmt yuv420p -an -movflags +faststart web_loom_ffmpeg.mp4 ``` --- ## 3. Render farm ### 3.1 Runtime configuration **Location:** `apps/render-farm/Dockerfile`. - Base image: Debian trixie, using Debian's ffmpeg. - Lines 47-49: ``` ENV RF_ENCODER=h264_nvenc ENV RF_NVENC_PRESET=p1 ENV RF_REQUIRE_GPU=1 ``` **Encoder selection and fallback:** - In code, `RF_ENCODER` defaults to `"libx264"` (`crates/render-farm/src/video.rs` line 425). Production sets NVENC through the Dockerfile. - There is no automatic NVENC to libx264 fallback. - If NVENC is unavailable, `find_by_name` fails (`"{encoder_name} unavailable"`). - `RF_REQUIRE_GPU` checks at `video.rs` lines 257 and 364 fail fast without a GPU. - libx264 is used only when `RF_ENCODER` is overridden. **Job defaults** (`apps/render-farm/src/coordinator.ts` lines 374-384): | Field | Value | |---|---| | compression | `Maximum` | | fps | 30 | | resolution | `[1920, 1080]` | | Worker threads per task | 8 (line 1067) | **Compression map** (`apps/render-farm/src/protocol.ts` `COMPRESSION_BPP`): | Compression | bpp | |---|---| | Maximum | 0.3 | | Social | 0.15 | | Web | 0.08 | | Potato | 0.04 | ### 3.2 RF-RENDER-NVENC and RF-RENDER-X264: chunked render encode **Location:** `crates/render-farm/src/video.rs` `encode`, lines 415-512. **Common settings, both encoders:** | Setting | Value | |---|---| | Input pixel format | NV12, or CUDA surfaces when NVENC and the renderer produced GPU frames (Linux) | | Time base | `1/fps` | | Frame rate | `fps/1` (CFR; frame-numbered) | | Colour | BT709 colorspace, MPEG (limited) range, BT709 primaries, BT709 trc | | Bitrate | `bit_rate = bitrate(W, H, fps, bpp)`, `max_bit_rate = B * 3 / 2` | | Flags | `GLOBAL_HEADER` | | K | `fps * 2` | **NVENC dictionary (production):** | Option | Value | |---|---| | preset | `RF_NVENC_PRESET` or `"p1"` | | tune | `RF_NVENC_TUNE` or `"hq"` | | rc | `constqp` with `qp=0` if tune is `"lossless"`, otherwise `vbr` | | spatial-aq | `RF_NVENC_SAQ` or `"1"` | | temporal-aq | `RF_NVENC_TAQ` or `"1"` | | bf | `0` | | g | K | | forced-idr | `1` | NVENC does not get the thread setting. **libx264 dictionary (override only):** | Option | Value | |---|---| | preset | `veryfast` | | threads | `8` | | bf | `0` | | rc-lookahead | `10` | | b-adapt | `0` (ignored) | | aq-mode | `1` | | ref | `2` (ignored) | | subme | `2` (ignored) | | trellis | `0` | | g | K | | keyint_min | K | The context threading count is also 8. **Rate control:** - NVENC: VBR with average B and max M. bufsize is not set, so NVENC's default VBV applies. - libx264: ABR at B, maxrate ignored (verified `rc=abr bitrate=8294` at 720p30, Maximum). **GOP:** - NVENC: K with forced IDR. Each HLS segment of `round(fps*2)` frames is one GOP. - libx264: keyint K, keyint_min clamped to K/2+1 (verified 60/31), scenecut 40, no B-frames. **Profile and level:** - NVENC: not set, so the encoder picks it. The preset decides profile; High is expected. - libx264: High, automatic level (verified High). **Container.** The farm writes its own boxes rather than using ffmpeg's muxer: - **MP4** (`apps/render-farm/src/mp4.ts`): - `buildHeader` (line 654) writes `ftyp` (major `isom`, minor `0x200`, brands `isom iso2 avc1 mp41`), then `moov`, `free`, `mdat`. The moov is up front. - Video timescale is `fps*1000` and sample delta is 1000 (CFR). - The `avc1` entry (line 584) carries `colr nclx 1/1/1/0` and `pasp`. - **HLS** (`RF_HLS` is on unless it is `"0"`; `RF_HLS_SEGMENT_SECONDS` defaults to 2; `coordinator.ts` lines 94-95): - Segments are fMP4 with `init.mp4` (line 1981) plus `styp+moof+mdat` `.m4s` files. - Segment keys are `${prefix}/c${chunk}-p${firstPart}-${index}.m4s` (`hls.ts` line 40). - `segmentFrames = round(fps*2)` (line 1087). - The playlist (`fmp4.ts` lines 267-285) has `#EXT-X-VERSION:7`, `#EXT-X-TARGETDURATION:` = 4, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:EVENT`, `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP`, `#EXTINF:` and `#EXT-X-ENDLIST`. **Audio:** AAC at 320,000 bit/s and 48,000 Hz (`crates/render-farm/src/audio.rs`). **Worked bitrates at Maximum (bpp 0.3):** | Resolution | B | M | |---|---|---| | 1920x1080@30 (job default) | 18,662,400 | 27,993,600 | | 1920x1080@60 | 29,859,841 | 44,789,761 | | 1280x720@30 | 8,294,400 | | | 3840x2160@30 | 74,649,602 | | Other compressions follow the table in section 0.2. **Reproduction on Linux.** RF-RENDER-X264 (verified; the x264 SEI matches `ref=1 subme=2 trellis=0 threads=8 keyint=60 keyint_min=31 rc_lookahead=10 rc=abr bitrate=8294`): ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -pix_fmt nv12 -c:v libx264 -preset veryfast -threads 8 -b:v B -maxrate M \ -bf 0 -rc-lookahead 10 -aq-mode 1 -trellis 0 -g K -keyint_min K \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \ -an -movflags +faststart rf_render_x264.mp4 ``` - `-maxrate` is included because the production code sets it. It has no effect without `-bufsize`. - The farm's own MP4 writer is approximated with `-movflags +faststart` (moov first). HLS approximation (verified to produce `#EXT-X-VERSION:7`, `PLAYLIST-TYPE:EVENT`, `INDEPENDENT-SEGMENTS`, `EXT-X-MAP init.mp4`). Replace the last line of the previous command with: ``` -f hls -hls_time 2 -hls_segment_type fmp4 -hls_playlist_type event \ -hls_fmp4_init_filename init.mp4 -hls_flags independent_segments out/rf.m3u8 ``` This approximation differs from production in two ways: - ffmpeg writes TARGETDURATION 2, while the farm writes 4. - Segment names differ. RF-RENDER-X264-INTENDED is not production. It shows what the dictionary was meant to do: add `-refs 2 -subq 2 -b_strategy 0`. RF-RENDER-NVENC cannot run on this VM. On an NVIDIA host: ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -pix_fmt nv12 -c:v h264_nvenc -preset p1 -tune hq -rc vbr -b:v B -maxrate M \ -spatial-aq 1 -temporal-aq 1 -bf 0 -g K -forced-idr 1 \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \ -an -movflags +faststart rf_render_nvenc.mp4 ``` ### 3.3 RF-TRANSCODE: source preparation before rendering **Location:** `apps/render-farm/src/transcode.ts`. | Lines | What | |---|---| | 7-49 | `transcodeArgs` | | 56 | `MAX_REMUX_KEYFRAME_GAP_SECONDS = 4` | | 77-111 | `canRemux` | | 113 | `remuxArgs` | `worker.ts` line 705 sets `TRANSCODE_ENCODER = process.env.RF_TRANSCODE_ENCODER ?? "h264_nvenc"`. `keyframeSeconds` is `TRANSCODE_KEYFRAME_SECONDS = 1` (`coordinator.ts` line 1551). **Remux path.** It is used when `canRemux` is true: - codec h264 - `yuv420p` - `has_b_frames=0` - every packet has pts == dts - keyframe gaps ≤ 4 s, including the tail ``` ffmpeg -hide_banner -nostdin -y -loglevel error -progress pipe:1 -nostats -i -map 0:v:0 -an -c:v copy -movflags +faststart ``` **Transcode path, literal** (with `keyframeSeconds = 1`): ``` ffmpeg -hide_banner -nostdin -y -loglevel error -progress pipe:1 -nostats [-hwaccel cuda] # NVENC only -i -map 0:v:0 -an -fps_mode passthrough -pix_fmt yuv420p -c:v NVENC: -preset p1 -rc vbr -cq 19 -b:v 0 libx264: -preset veryfast -crf 18 -bf 0 -force_key_frames "expr:gte(t,n_forced*1)" [-forced-idr 1] # NVENC only -movflags +faststart ``` **Encoder settings:** | Setting | NVENC (production default) | libx264 | |---|---|---| | Rate control | Constant-quality VBR, `-cq 19`, `-b:v 0` | CRF 18 | | Preset | p1 | veryfast | | Keyframes | Forced every 1 s, as IDR | Forced every 1 s, on top of x264 keyint 250 / min 25 / scenecut 40 | | B-frames | None | None | - **Profile and level:** encoder automatic. libx264 verified High. - **Frame rate:** `-fps_mode passthrough` keeps the source VFR timestamps. - **Colour:** passed through. - **Container:** MP4 `+faststart`, video only. - **Hardware decode:** `-hwaccel cuda` decodes on the GPU but frames are downloaded (no `-hwaccel_output_format`), so `-pix_fmt yuv420p` applies. **Reproduction on Linux.** RF-TRANSCODE-X264 (verified keyframes at 0, 1, 2 s, bframes=0, High): ``` ffmpeg -hide_banner -nostdin -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -map 0:v:0 -an -fps_mode passthrough -pix_fmt yuv420p -c:v libx264 -preset veryfast -crf 18 \ -bf 0 -force_key_frames "expr:gte(t,n_forced*1)" -movflags +faststart rf_transcode_x264.mp4 ``` RF-TRANSCODE-NVENC cannot run on this VM: ``` ffmpeg -hide_banner -nostdin -y -hwaccel cuda -i in.mp4 -map 0:v:0 -an -fps_mode passthrough \ -pix_fmt yuv420p -c:v h264_nvenc -preset p1 -rc vbr -cq 19 -b:v 0 -bf 0 \ -force_key_frames "expr:gte(t,n_forced*1)" -forced-idr 1 -movflags +faststart rf_transcode_nvenc.mp4 ``` --- ## 4. Desktop recording ### 4.1 Common facts **Defaults** (`crates/recording/src/defaults.rs`): | Constant | Value | |---|---| | `DEFAULT_STUDIO_MAX_FPS` | 60 | | `CAMERA_ACTIVE_STUDIO_MAX_FPS` | 30 | | `DEFAULT_INSTANT_MODE_FPS` | 30 | | `FREE_INSTANT_MODE_MAX_RESOLUTION` | 1280 | | `PRO_INSTANT_MODE_MAX_RESOLUTION` | 1920 | | `DEFAULT_CRASH_RECOVERY_RECORDING` | true, so fragmented recording is the default | | `DEFAULT_OUT_OF_PROCESS_MUXER` | false | - Default studio quality is `Compatibility` if total RAM is under 16 GiB, else `Balanced`. `Ultra` is opt-in. - Instant mode clamps the output to fit `(max, max*9/16)`: 1280x720 for free users, 1920x1080 for Pro (`instant_recording.rs` lines 719-734). **Timestamps (all `enc-ffmpeg` recording paths):** - `VideoInfo.time_base = 1/1_000_000` (`crates/media-info/src/lib.rs` lines 47, 58, 89). - `update_pts` (`crates/enc-ffmpeg/src/base.rs` lines 76-104) sets `pts = round(capture_ts_secs * 1e6) - first_pts`. - So output is VFR, following capture timestamps. - `frame_rate` is the capture rate. It is used only for bitrate and K. **Fragmented container.** `SegmentedVideoEncoder` (`crates/enc-ffmpeg/src/mux/segmented_stream.rs` lines 195-240) uses the ffmpeg `dash` muxer writing `dash_manifest.mpd`: | Muxer option | Value | |---|---| | `init_seg_name` | `init.mp4` | | `media_seg_name` | `segment_$Number%03d$.m4s` | | `seg_duration` | `` | | `use_timeline` | `1` | | `use_template` | `1` | | `single_file` | `0` | | `hls_playlist` | `1` | Output files, verified: `init.mp4`, `segment_001.m4s`, ..., `master.m3u8`, and `media_0.m3u8` (`#EXT-X-VERSION:6` with `PROGRAM-DATE-TIME`). ### 4.2 Linux (X11 capture) **Location:** `crates/recording/src/capture_pipeline.rs` lines 374-430, and `crates/recording/src/output_pipeline/ffmpeg.rs`. **Studio:** `SegmentedVideoMuxer`, 2 s segments, preset `Medium` when quality is Ultra, otherwise `Ultrafast`. **Instant:** `SegmentedVideoMuxer`, 2 s segments, preset `Ultrafast`, output size clamped as in section 4.1. **bpp is always 0.3.** `start_encoder` (`ffmpeg.rs` lines 648-672) hardcodes `bpp: H264EncoderBuilder::QUALITY_BPP` (0.3) for every quality and for instant mode. Instant mode does not use 0.15 here, unlike macOS and Windows. `prefer_videotoolbox_hw_input` is false. **Camera:** `SegmentedVideoMuxer` with a 3 s segment (line 825). The muxer default is 3 s (line 471). **Encoder:** `/dev/nvidiactl` present gives `h264_nvenc` (recording set: p4, ll, vbr, AQ, `g=K`), otherwise `libx264`. **libx264 settings:** | Quality | Preset / tune | |---|---| | Balanced or Compatibility | `ultrafast` + `zerolatency` | | Ultra (preset Medium) | `veryfast` + `zerolatency` | | Setting | Value | |---|---| | Rate control | ABR at B (bpp 0.3), maxrate ignored | | GOP | keyint K, keyint_min clamped to K/2+1 | | Scenecut | 0 (zerolatency) | | B-frames | None | | Pixel format | yuv420p, converted from capture format by swscale (BT.709, full-range in, limited out) | | Colour | BT.709 tags | **Verified ultrafast/zerolatency output at 720p30:** - Constrained Baseline, level 3.2 - `cabac=0 ref=1 subme=0 trellis=0 8x8dct=0 sliced_threads=1 bframes=0 keyint=60 keyint_min=31 scenecut=0 rc=abr mbtree=0 aq=0` **Worked B at bpp 0.3:** | Resolution | B | |---|---| | 1280x720@30 | 8,294,400 | | 1920x1080@30 | 18,662,400 | | 1920x1080@60 | 29,859,841 | | 3840x2160@30 | 74,649,602 | K is 60 at 30 fps and 120 at 60 fps. **Audio:** AAC 320 kbps via `enc-ffmpeg`. **Reproduction on Linux.** DESK-REC-X264 (verified): ``` mkdir -p out && ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -c:v libx264 -preset ultrafast -tune zerolatency -threads N -b:v B -maxrate M -g K -keyint_min K \ -pix_fmt yuv420p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -an \ -f dash -seg_duration 2 -use_timeline 1 -use_template 1 -single_file 0 -hls_playlist 1 \ -init_seg_name init.mp4 -media_seg_name 'segment_$Number%03d$.m4s' out/dash_manifest.mpd ``` DESK-REC-X264-ULTRA: the same command with `-preset veryfast`. B stays at bpp 0.3 on Linux. Difference from production: the raw input is CFR. Production uses VFR capture timestamps in a 1/1e6 time base. ### 4.3 macOS (cannot run on this VM) **Fragmented studio (default)** (`capture_pipeline.rs` lines 113-226): - Muxer: `MacOSFragmentedM4SMuxer`, or the out-of-process muxer when enabled. Both use `SegmentedVideoEncoder`. - `prefer_videotoolbox_hw_input` is true for screen (`macos_fragmented_m4s.rs` line 295) and false for camera (line 682). - Defaults (lines 185-187): 2 s segments, `Ultrafast`, bpp 0.3. | Quality | bpp | Preset | |---|---|---| | Ultra | 1.0 | Medium | | Compatibility | 0.15 | Ultrafast | | Balanced | 0.3 | Ultrafast | **DESK-REC-VT** (`h264_videotoolbox`, recording set): | Setting | Value | |---|---| | Options | `realtime=true`, `prio_speed=true`, `profile=main` | | Bitrate | B; `DataRateLimits` = M/8 bytes per 1 s | | GOP | 12 frames (FFmpeg default; Cap sets no `g`) | | B-frames | None (frame reordering off) | | Level | Main AutoLevel | | Input | NV12 hardware surfaces, no scaling | | Colour | BT.709 tags | | Fallback | libx264 (section 0.6 recording set) when VideoToolbox fails to open or fps exceeds 0.9 times the estimated VideoToolbox maximum | Platform command (not runnable here): ``` ffmpeg -f rawvideo -pix_fmt nv12 -s WxH -r FPS -i in.nv12 -c:v h264_videotoolbox -realtime true -prio_speed true -profile:v main -b:v B -maxrate M -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv out.mp4 ``` Production passes no `-g`, so the ffmpeg CLI default `gop_size` (12) also applies here. **Non-fragmented studio:** `AVFoundationMp4Muxer` (`crates/recording/src/output_pipeline/macos.rs` lines 326-352) builds `cap_enc_avfoundation::MP4Encoder` (`crates/enc-avfoundation/src/mp4.rs`). | Setting | Value | |---|---| | Settings base | `OutputSettingsAssistant::with_preset(h264_3840x2160)` (line 251) | | Output height | `ensure_even(output_height or source height)` | | Output width | Proportional, even (lines 260-268) | | Bitrate | `AVVideoAverageBitRateKey` from the formulas below | | `AVVideoAllowFrameReorderingKey` | `ultra && !instant` | | `AVVideoExpectedSourceFrameRateKey` | fps | | `AVVideoMaxKeyFrameIntervalKey` | `floor(0.75 * fps).max(1)` | | Colour | `ITU_R_709_2` for transfer, primaries and matrix (lines 325-339) | | Writer | `expects_media_data_in_real_time` true | | Profile, level, entropy coding | AVFoundation defaults | Bitrate formulas (lines 1070-1109), with `pixels = W*H` and `f = min(fps, 60)/30`: | Profile | Formula | |---|---| | Balanced | `max(pixels*f*5, 8,000,000)` | | Ultra | `clamp(pixels*f*10, 8,000,000, 120,000,000)` | | Compatibility | `clamp(pixels*f*2.5, 2,500,000, 10,000,000)` | | Instant | `1,500,000 + pixels/(1920*1080)*1,500,000 + f*500,000` | Worked values: | Profile | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | Balanced | 8,000,000 | 10,368,000 | 20,736,000 | 41,472,000 | | Ultra | 9,216,000 | 20,736,000 | 41,472,000 | 82,944,000 | | Compatibility | 2,500,000 | 5,184,000 | 10,000,000 | 10,000,000 | | Instant | 2,666,666 | 3,500,000 | 4,000,000 | 8,000,000 | Keyframe interval: 22 at 30 fps, 45 at 60 fps. **Instant mode:** - Screen: `MacOSFragmentedM4SMuxer` with bpp 0.15 through VideoToolbox, 2 s segments. - Camera-only instant: AVFoundation `init_instant_mode` (instant formula, no reordering). **Studio camera:** `AVFoundationCameraMuxer` (`macos.rs` lines 973-995), using the Compatibility profile when the compatibility flag is set, otherwise Balanced. Reordering is off. AVFoundation reproduction is a platform API (`AVAssetWriter` with the keys above). There is no ffmpeg equivalent. ### 4.4 Windows (cannot run on this VM) **Fragmented studio (default)** (`capture_pipeline.rs` lines 247-369, `win_fragmented_m4s.rs`): - `SegmentedVideoEncoder` with 2 s segments (line 232). The camera uses 3 s (line 659). - Hardware input is false. | Quality | bpp | Preset | |---|---|---| | Ultra | 1.0 | Medium | | Other | 0.3 | Ultrafast | The encoder follows the Windows priority in section 0.5, with the recording option sets from section 0.6. NVENC example (platform command): ``` ffmpeg ... -c:v h264_nvenc -preset p4 -tune ll -rc vbr -spatial-aq 1 -temporal-aq 1 -g K -b:v B -maxrate M -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -f dash ... ``` **Instant:** 2 s segments, `Ultrafast`, bpp 0.15. **Non-fragmented studio:** `WindowsMuxer` (`output_pipeline/win.rs`). | Setting | Value | |---|---| | `frame_rate` | 30 | | `bitrate_multiplier` | 0.3 for Ultra, else 0.15 | | `fragmented` | false | | `frag_duration_us` | 2,000,000 | - Encoder: `cap_enc_mediafoundation::H264Encoder` (`crates/enc-mediafoundation/src/video/h264.rs`). - `MF_MT_AVG_BITRATE` (line 359) = `calculate_bitrate` (lines 693-696): `W*H*((max(fps-30,0)/2)+30)*multiplier`, with fps 30. - `MF_MT_FRAME_RATE` = `frame_rate/1` (line 366). - No GOP, profile or rate-control mode is set, so these are Media Foundation transform defaults. - Frame pacing at `1/frame_rate` (`win.rs` line 340) gives CFR at 30 fps. Worked MF bitrates (fps fixed at 30): | Multiplier | 1280x720 | 1920x1080 | 1920x1080 (60 fps capture) | 3840x2160 | |---|---|---|---|---| | 0.3 | 8,294,400 | 18,662,400 | 18,662,400 | 74,649,600 | | 0.15 | 4,147,200 | 9,331,200 | 9,331,200 | 37,324,800 | **Software fallback** (`win.rs` lines 229-243): `H264Encoder::builder(video_config)` with builder defaults (bpp 0.3, `Ultrafast`, recording mode, default priority). It is used when: - software is forced - the primary GPU is AMD (so AMD goes through `h264_amf` with `quality=balanced`, `rc=vbr_latency` first) - Media Foundation init or validation fails **Camera:** Media Foundation multiplier 0.2 at the camera fps (`win.rs` line 763). --- ## 5. Desktop export **Location:** `crates/export/src/mp4.rs`. **Compression map:** | Compression | bpp (lines 147-152) | CRF (lines 156-161) | |---|---|---| | Maximum | 0.3 | 24 | | Social | 0.15 | 28 | | Web | 0.08 | 32 | | Potato | 0.04 | 36 | **`Mp4ExportSettings`** (lines 180-190): | Field | Notes | |---|---| | `fps` | | | `resolution_base` | | | `compression` | | | `custom_bpp` | | | `force_ffmpeg_decoder` | | | `optimize_filesize` | serde default false | - `effective_bpp` (line 193) is `custom_bpp` if set, else the map. - `VideoInfo` is NV12 with `time_base = 1/fps` (lines 365-367). Frames are numbered, so output is CFR. - Builder chain: `with_bpp`, `with_export_priority` (`is_export = true`), `with_export_settings`, `with_external_conversion`, plus `with_crf(crf)` if `optimize_filesize`. - The builder preset stays at its default `Ultrafast`, which selects the libx264 "fast" set. - Container: `MP4File::init("output", path, self.optimize_filesize, ...)`. `+faststart` is written only when `optimize_filesize` is true (`crates/enc-ffmpeg/src/mux/mp4.rs` lines 44-90). Default exports have the moov at the end. - Audio: AAC 320 kbps. - macOS: `want_hw_input = !optimize_filesize && frame is a Surface`. **UI defaults** (`apps/desktop/src/routes/editor/ExportPage.tsx`): | Setting | Value | |---|---| | Format | Mp4 | | fps | 30 (options 15/30/60, lines 79-83) | | Resolution | 720p 1280x720 | | Compression | Maximum | | `optimizeFilesize` | false | | Custom bpp slider | 0.02 to 0.5, step 0.01 (lines 1408-1410) | - Resolution options (`Header.tsx` lines 33-37): 720p 1280x720, 1080p 1920x1080, 4K 3840x2160. - Output size is rounded: width up to a multiple of 4, height up to even (`crates/rendering/src/lib.rs` `get_output_size`, lines 3228-3242). ### 5.1 DESK-EXPORT-X264-{Maximum,Social,Web,Potato}: default export on Linux, or any platform's software fallback Encoder settings: libx264 export "fast" set plus `g=K`, `keyint_min=K`, `threads=N`. | Setting | Value | |---|---| | Rate control | ABR at B, maxrate ignored | | GOP | K, min K/2+1 | | B-frames | None | | Preset / tune | veryfast, no tune | | Effective internals | ref=1, subme=2, trellis=0, rc_lookahead=10 | | Profile / level | High, automatic level | | Colour | BT.709 limited | | Container | MP4 without faststart; track time base 1/90000 | Verified at 720p30 Maximum: SEI identical to the render farm except `threads=4`. Worked B per quality (Rust model, section 0.2): | Compression | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | Maximum | 8,294,400 | 18,662,400 | 29,859,841 | 74,649,602 | | Social | 4,147,200 | 9,331,200 | 14,929,920 | 37,324,801 | | Web | 2,211,839 | 4,976,639 | 7,962,623 | 19,906,559 | | Potato | 1,105,919 | 2,488,319 | 3,981,311 | 9,953,279 | K is 60 at 30 fps and 120 at 60 fps. The desktop UI also offers 15 fps, where K = 30. **Reproduction on Linux (verified):** ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -pix_fmt nv12 -c:v libx264 -preset veryfast -threads N -b:v B -maxrate M \ -bf 0 -rc-lookahead 10 -aq-mode 1 -trellis 0 -g K -keyint_min K \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \ -an -video_track_timescale 90000 desk_export_x264.mp4 ``` ### 5.2 DESK-EXPORT-CRF-{24,28,32,36}: "Optimize file size" CRF forces libx264. Dictionary: `preset=slow`, `crf=<24|28|32|36>`, `pix_fmt=yuv420p` (ignored), `g=K`, `keyint_min=K`. Threading is N through the context. No bitrate is set (`bit_rate = 0`). | Setting | Value | |---|---| | Rate control | CRF | | GOP | K, min K/2+1 | | B-frames | x264 slow default: bframes=3, b_adapt=1 | | Preset | slow | | Effective internals | ref=5, subme=8, trellis=2, rc_lookahead=50 | | Profile | High | | Colour | BT.709 limited | | Container | MP4 with `+faststart` | Verified at 720p30, CRF 24: `ref=5 subme=8 trellis=2 threads=4 bframes=3 keyint=60 keyint_min=31 rc_lookahead=50 rc=crf crf=24.0`. Compression maps to CRF as Maximum 24, Social 28, Web 32, Potato 36. **Reproduction on Linux (verified):** ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -pix_fmt nv12 -c:v libx264 -preset slow -crf 24 -threads N -g K -keyint_min K \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \ -an -movflags +faststart -video_track_timescale 90000 desk_export_crf24.mp4 ``` ### 5.3 Hardware export variants (cannot run on this VM) All variants use the same B and M as section 5.1. **DESK-EXPORT-VT (macOS default):** - Options: `realtime=false`, `profile=main`, `allow_sw=0`; b B, maxrate M. - GOP is 12 frames because no `g` is set. There are no B-frames. - Input is NV12 surfaces. ``` ffmpeg ... -c:v h264_videotoolbox -realtime false -profile:v main -allow_sw 0 -b:v B -maxrate M ... ``` **DESK-EXPORT-NVENC** (Windows NVIDIA, or Linux with `/dev/nvidiactl`): ``` -c:v h264_nvenc -preset p5 -tune hq -rc vbr -spatial-aq 1 -temporal-aq 1 -b_ref_mode middle -g K -b:v B -maxrate M ``` **DESK-EXPORT-QSV:** ``` -c:v h264_qsv -preset medium -look_ahead 1 -look_ahead_depth 20 -g K -b:v B -maxrate M ``` **DESK-EXPORT-AMF:** ``` -c:v h264_amf -quality quality -rc vbr_peak -g K -b:v B -maxrate M ``` **DESK-EXPORT-MF:** ``` -c:v h264_mf -hw_encoding true -scenario 0 -quality 0 -g K -b:v B -maxrate M ``` --- ## 6. Web recorders on `main` (MediaRecorder) ### 6.1 BR-MR-MAIN: dashboard web recorder **Locations:** - `packages/recorder-core/src/recorder-constants.ts` - `packages/recorder-core/src/recorder-utils.ts` - `apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts` **Capture constraints** (`recorder-constants.ts` lines 9-19): `{frameRate:{ideal:30}, width:{ideal:1920}, height:{ideal:1080}}`. These are ideal values, not exact. **MIME candidates:** | List | Candidates | |---|---| | `WEBM_MIME_TYPES` (lines 94-97), withAudio | `video/webm;codecs=vp9,opus`, `video/webm;codecs=vp8,opus` | | `WEBM_MIME_TYPES`, videoOnly | `video/webm;codecs=vp9`, `video/webm;codecs=vp8`, `video/webm` | | `MP4_MIME_TYPES` (lines 82-92), withAudio | `video/mp4;codecs="avc1.42E01E,mp4a.40.2"`, `video/mp4;codecs="avc1.4d401e,mp4a.40.2"` | | `MP4_MIME_TYPES`, videoOnly | `video/mp4;codecs="avc1.42E01E"`, `video/mp4;codecs="avc1.4d401e"`, `video/mp4` | **Pipeline selection** (`selectRecordingPipelineFromSupport`, lines 140-190): 1. WebM is supported and `preferStreamingUpload !== false`: mode `streaming-webm`. 2. Otherwise the first supported MP4 candidate: mode `buffered-raw`. 3. Otherwise WebM in mode `buffered-raw`. `shouldPreferStreamingUpload` (lines 105-130) returns false for iOS, Firefox and Safari, and true for Chromium brands. **Recorder options** (`getMediaRecorderOptions`, lines 36-41): ``` { mimeType, videoKeyFrameIntervalDuration: 2_000 } ``` No `videoBitsPerSecond` is set, so the browser default applies (Chrome: 2.5 Mbps video, 128 kbps audio). It is constructed at `useWebRecorder.ts` line 1101. **Timeslice:** - `streaming-webm`: `start(1000)` (`INSTANT_UPLOAD_REQUEST_INTERVAL_MS`, line 75). - If that throws: `start()` with manual flushing. - `buffered-raw`: `start(200)` (line 1141). **What each browser records:** | Browser | Output | |---|---| | Chrome / Edge | VP9 + Opus WebM. This is not H.264. | | Safari | H.264 MP4, `avc1.42E01E,mp4a.40.2` (Constrained Baseline 3.0 requested; Safari's own encoder) | | Firefox | First supported MP4 candidate, else WebM | `videoInstantCreate` is called with `videoCodec: "h264"` (line 1056). **Server step:** - Chrome VP9 is transcoded by MS-TRANSCODE-VPX: libx264 medium, CRF 23, `-bf 0`, capped at 1080p, level 4.2, CFR, AAC 128k. - Safari H.264 at or below 1080p and level 4.2 is copied. - `recording-conversion.ts` `convertToMp4` (line 127) is unused. **Reproduction on Linux.** Chrome via Playwright: ```js new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9,opus", videoKeyFrameIntervalDuration: 2000 }) recorder.start(1000) ``` Then apply MS-TRANSCODE-VPX to the WebM (use `-i rec.webm` in place of the rawvideo input). **BR-MR-MAIN-MP4 (Safari-path equivalent in Chrome):** `{ mimeType: 'video/mp4;codecs="avc1.42E01E"', videoKeyFrameIntervalDuration: 2000 }` with `start(200)`. Linux Chrome has no AAC MediaRecorder encoder, so the `mp4a.40.2` variants are unsupported there. ### 6.2 BR-MR-EXT-MAIN: Chrome extension **Location:** `apps/chrome-extension/src/offscreen/recorder.ts`. - `new MediaRecorder(recordingStream, { mimeType: pipeline.mimeType })` at lines 944-946. - No keyframe or bitrate options. - `start(RECORDING_TIMESLICE_MS)` with `RECORDING_TIMESLICE_MS` = 1000 (lines 70, 1086). - In Chrome this gives VP9/Opus WebM at the Chrome default bitrate, then MS-TRANSCODE-VPX. Reproduction: `new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9,opus" }); recorder.start(1000)`. ### 6.3 BR-MR-TOOLS: SpeedController and TrimmingTool **Location:** `apps/web/components/tools/SpeedController.tsx` and `TrimmingTool.tsx`. - Source: `canvas.captureStream(60)` (SpeedController line 243, TrimmingTool line 408). - Options: `{ mimeType: selectedMimeType, videoBitsPerSecond: 5000000 }` (SpeedController line 263, TrimmingTool line 446). - Timeslice: `start(1000)` in SpeedController (line 296), `start(100)` in TrimmingTool (line 485). MIME list for MP4 input (non-MP4 input uses the same WebM entries first, then `video/mp4`): ``` "video/mp4", "video/webm;codecs=vp9,opus", "video/webm;codecs=vp8,opus", "video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm" ``` Reproduction: `new MediaRecorder(canvas.captureStream(60), { mimeType: "video/mp4", videoBitsPerSecond: 5000000 })`. The codec Chrome picks for bare `video/mp4` is browser-dependent. --- ## 7. Browser WebCodecs encoders on `main` ### 7.1 BR-WC-REMOTION: Remotion `convertMedia` **Packages:** `@remotion/webcodecs` 4.0.484 and mediabunny 1.49.0, unpacked to `/tmp` for reading. **Callers:** - `apps/web/app/s/[videoId]/_components/timeline/recording/convert-comment-media.ts` lines 25-32: `convertMedia({ container: "mp4", videoCodec: "h264", audioCodec: "aac" })` with `installAvcLevelClamp()` active. It skips conversion when the input is already MP4 or `canConvertToMp4InBrowser` (probe `avc1.42E01F` 1280x720 plus AAC) fails. - `apps/web/components/tools/MediaFormatConverter.tsx` lines 650-657: `convertMedia({ container: "mp4", videoCodec: "h264" })` with no clamp. **VideoEncoderConfig built by Remotion:** | Field | Value | |---|---| | bitrate | Not set in Chrome. Safari: 3,000,000. | | framerate | Source fps | | hardwareAcceleration | Tries `prefer-hardware`, then `prefer-software` | | Keyframes | Every 40 frames | | Codec | `avc1.64` + `00` + level from Remotion's width/height/fps table (profile byte always 0x64) | **Level clamp.** `installAvcLevelClamp` (`avc-level-clamp.ts`) patches `VideoEncoder.prototype.configure` to replace the codec with `pickMobileSafeAvcCodec(width, height)`: | Size | Codec | |---|---| | ≤2048x1088 | `avc1.64002A` | | ≤4096x2304 | `avc1.640033` | | Larger | `avc1.640034` | **Chromium default bitrate** when none is given (`media/base/video_encoder.cc` `GetDefaultVideoEncodeBitrate`, source only): ``` 2,000,000 * clamp(fps,1,300) * clamp(W*H,1,64M) / (1280*720*30) ``` The result is clamped to at least 10,000. **Worked values:** | | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | Chromium default bitrate | 2,000,000 | 4,500,000 | 9,000,000 | 18,000,000 | | Codec without clamp | `avc1.64001F` | `avc1.64002A` | `avc1.64002A` | `avc1.640034` | | Codec with clamp | `avc1.64002A` | `avc1.64002A` | `avc1.64002A` | `avc1.640033` | **Container:** MP4 written by Remotion. Audio is AAC; Linux Chrome has no AAC AudioEncoder, so comment media with audio is left as WebM there. **Reproduction on Linux** (Playwright, Google Chrome): ```js new VideoEncoder(...).configure({ codec: "avc1.64002A", // with clamp; "avc1.64001F" etc. without width: W, height: H, framerate: FPS, hardwareAcceleration: "prefer-software", }) ``` Set `keyFrame: true` every 40th frame and leave `bitrate` unset. Headless Linux Chrome rejects `prefer-hardware` (verified), so Remotion's software retry is the path that runs. ### 7.2 BR-WC-MEDIABUNNY-LOOM: Loom browser conversion **Location:** `apps/web/lib/loom-browser-conversion.ts` (same on `main` and the PR). DASH WebM path (lines 644-662): ```js new Mp4OutputFormat({ fastStart: false }) new VideoSampleSource({ codec: "avc", bitrate: QUALITY_HIGH, hardwareAcceleration: "prefer-hardware", alpha: "discard" }) new AudioSampleSource({ codec: "aac", bitrate: QUALITY_MEDIUM }) ``` Conversion path (lines 857-873): ```js Mp4OutputFormat({ fastStart: false }) Conversion.init({ video: { codec: "avc", bitrate: QUALITY_HIGH, hardwareAcceleration: "prefer-hardware" }, audio: { codec: "aac", bitrate: QUALITY_MEDIUM }, }) ``` **mediabunny 1.49.0 behaviour (source):** - Quality factors: VERY_LOW 0.3, LOW 0.6, MEDIUM 1, HIGH 2, VERY_HIGH 4. - Video bitrate: `ceil(3e6 * (W*H/2073600)^0.95 * 1.0(avc) * 2 / 1000) * 1000`. It does not depend on fps. - `keyFrameInterval` defaults to 2 s, by timestamp. - `latencyMode` defaults to quality. - Codec is `avc1.6400LL` from mediabunny's AVC level table, chosen by macroblocks and bitrate. - The config adds `avc: { format: "avc" }`. - Timestamps are passed through from the source (VFR). - AAC QUALITY_MEDIUM is 128,000 bit/s. **Worked values:** | | 1280x720 | 1920x1080 (any fps) | 3840x2160 | |---|---|---|---| | Video bitrate | 2,778,000 | 6,000,000 | 22,393,000 | | Codec | `avc1.64001F` | `avc1.640028` | `avc1.640033` | **Container:** MP4 with the moov at the end (`fastStart: false`). **Reproduction on Linux:** ```js VideoEncoder.configure({ codec: "avc1.640028", width: 1920, height: 1080, bitrate: 6000000, framerate: FPS, hardwareAcceleration: "no-preference", alpha: "discard", avc: { format: "avc" }, }) ``` `prefer-hardware` is unsupported in headless Linux Chrome (verified). Force a keyframe every 2 s of timestamp. --- ## 8. PR 2312 differences (`FETCH_HEAD` 549c5afa9) The PR does not change: - `crates/enc-ffmpeg` - `crates/export/src/mp4.rs` - `crates/render-farm/src/video.rs` - the desktop capture pipelines ### 8.1 BR-MR-PR-SCREEN, BR-MR-PR-CAMERA, BR-MR-PR-OVERLAY: web recorder **Location (PR):** `packages/recorder-core/src/recorder-encoding.ts`. **MIME candidates:** `recorder-constants.ts` lines 96-102 add: | List | Candidates | |---|---| | `STREAMING_MP4_MIME_TYPES`, withAudio | `video/mp4;codecs="avc1.64002A,mp4a.40.2"`, `video/mp4;codecs="avc1.64002A,opus"` | | `STREAMING_MP4_MIME_TYPES`, videoOnly | `video/mp4;codecs="avc1.64002A"` | **Pipeline selection** (`recorder-utils.ts` lines 139-211): - If `preferStreamingUpload !== false`: streaming MP4, else streaming WebM. Both use mode `"streaming"`. - Otherwise buffered MP4 (`avc1.42E01E` list), else buffered WebM. - `getMediaRecorderOptions` is removed. **Bitrate tiers** (`BITRATES`, lines 45-49), in bit/s for captures with ≤1280x720, ≤1920x1088, ≤2560x1600 and larger pixel counts: | Content | ≤720p | ≤1080p | ≤1600p | Larger | |---|---|---|---|---| | screen | 2,500,000 | 4,000,000 | 6,000,000 | 10,000,000 | | camera | 2,600,000 | 4,500,000 | 6,500,000 | 10,000,000 | | cameraOverlay | 2,000,000 | 3,500,000 | 5,000,000 | 8,000,000 | - `recordingBitrate` (lines 57-74): above 40 fps, `Math.round(base * 1.5)`. - Missing width/height default to 1920x1080. Missing fps defaults to 30. **Recorder options** (`recorderOptions`, lines 87-117): ``` { mimeType, videoBitsPerSecond: round(tier * bitrateScale * (vp8 ? 1.3 : 1)), videoKeyFrameIntervalDuration: 2000 } ``` - For a `video/mp4` MIME containing `avc1.xxxxxx`, the codec is rewritten to `pickMobileSafeAvcCodec(track W, H)`: `64002A`, `640033` or `640034`. The rewrite is kept only if `isTypeSupported`. - The `avc1.42E01E` Safari candidate is also rewritten. - W, H and fps come from `track.getSettings()`. **Quality settings** (`web-recorder-dialog/recording-quality.ts`, PR): | Setting | Value | |---|---| | Default | `screenHeight` 1080, `frameRate` 30, `cameraHeight` 1080, `level` standard | | Screen height options | 1080 / 1440 / 2160 | | fps options | 30 / 60 | | Camera height options | 720 / 1080 | | `qualityBitrateScale` | high = 1.6, standard = 1 | | Storage | `localStorage` `"cap-web-recorder-quality"` | **Capture constraints** (`capture-streams.ts`): `{ frameRate: { ideal: q.frameRate ?? 30 }, width: { ideal: round(h*16/9) }, height: { ideal: h } }`. `DEFAULT_CAMERA_QUALITY` is 1080@30. **useWebRecorder (PR):** - The display recorder uses `recorderOptions(pipeline.mimeType, mixedStream.getVideoTracks()[0], isSupported, qualityBitrateScale(quality), recordingMode === "camera" ? "camera" : "screen")` (lines 1753-1762 of the PR file). - The separate camera recorder records a video-only stream with `"cameraOverlay"` (lines 1764-1775). - `selectPairedCameraPipeline` (lines 77-93): if the display pipeline is buffered-raw MP4 (Safari), the camera uses `video/webm;codecs=vp8` in streaming mode, so the 1.3 VP8 scale applies. - Timeslice: `start(1000)` for streaming, else `start(200)`. The camera recorder uses `start(1000)`. **Worked videoBitsPerSecond:** | Content | Scale | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---|---| | screen | standard | 2,500,000 | 4,000,000 | 6,000,000 | 10,000,000 | | screen | high (1.6) | 4,000,000 | 6,400,000 | 9,600,000 | 16,000,000 | | camera | standard | 2,600,000 | 4,500,000 | 6,750,000 | 10,000,000 | | camera | high (1.6) | 4,160,000 | 7,200,000 | 10,800,000 | 16,000,000 | | cameraOverlay | standard | 2,000,000 | 3,500,000 | 5,250,000 | 8,000,000 | | cameraOverlay | high (1.6) | 3,200,000 | 5,600,000 | 8,400,000 | 12,800,000 | | cameraOverlay, VP8 (Safari camera) | standard | 2,600,000 | 4,550,000 | 6,825,000 | 10,400,000 | At 2560x1440@30 (the PR's 1440 option), screen standard is 6,000,000. **Observed Chrome 148 behaviour on Linux (verified, canvas source, 1280x720, 2,500,000 bit/s, `avc1.64002A`, 2 s keyframes):** | Property | Observed | |---|---| | Reported `recorder.mimeType` | `video/mp4;codecs=avc1.640020`. Chrome picked level 3.2 itself. | | Profile | High | | B-frames | `has_b_frames=0` | | Colour tags | smpte170m, tv range | | Container | Fragmented MP4: `ftyp`, `moov`, then `moof`/`mdat` per 1 s timeslice | | Timestamps | VFR (avg 29.8 fps) | | Keyframes | At 0, 2.03 and 4.06 s, plus extra encoder-chosen IDRs on scene changes | | Achieved rate | 1.94 Mbit/s on synthetic content | | Supported withAudio MIME | `avc1.64002A,opus` (no AAC) | **Server step:** - PR media server: H.264 at or below 1080p and level 4.2 has its video copied; Opus audio is transcoded to AAC 128k. - 1440p and 2160p captures are transcoded down to 1080p with MS-TRANSCODE. The PR `process-video` still passes no size options. - Render-farm sources from these recordings usually take the remux path, because they have 2 s keyframes and no B-frames. **Reproduction on Linux** (Playwright, Google Chrome, fake-camera track from `in.y4m`; see section 9): ```js // BR-MR-PR-SCREEN at 1920x1080@30, standard new MediaRecorder(stream, { mimeType: 'video/mp4;codecs="avc1.64002A,opus"', videoBitsPerSecond: 4000000, videoKeyFrameIntervalDuration: 2000 }); rec.start(1000); // BR-MR-PR-CAMERA at 1920x1080@30: videoBitsPerSecond 4500000 // BR-MR-PR-OVERLAY at 1920x1080@30: videoBitsPerSecond 3500000 // 3840x2160: mimeType 'video/mp4;codecs="avc1.640033,opus"' (only if isTypeSupported), bitrate per table ``` ### 8.2 Chrome extension (PR) `recorder.ts` lines 1318-1340 (PR): - `recorderOptions(pipeline.mimeType, recordingStream.getVideoTracks()[0], isSupported, 1, request.mode === "camera" ? "camera" : "screen")` for the main track. - `"cameraOverlay"` for the camera track. - Timeslice 1000. The bitrates match the standard rows of the table in section 8.1. ### 8.3 BR-MR-CLIP-PR: editor clip recorder **Location:** `apps/web/lib/editor-clip-recorder.ts`. - `new MediaRecorder(stream, { mimeType })`, with the MIME from `selectRecordingPipelineFromSupport`. - No bitrate or keyframe option. - `start(1000)`. - In Chrome this records `avc1.64002A` streaming MP4 at the Chrome default (2.5 Mbps). Reproduction: `new MediaRecorder(stream, { mimeType: 'video/mp4;codecs="avc1.64002A,opus"' }); rec.start(1000)`. ### 8.4 BR-WC-EXPORT-PR: browser local export (WebCodecs) **Locations (PR):** - `packages/editor-solid-web/src/browser-export-worker.ts` lines 779-806 - `browser-export-encoder.ts` - `browser-export-estimate.ts` - `browser-export-audio.ts` - `browser-local-export.ts` **Job inputs:** - fps is clamped to 1-120. - `bitsPerPixel` is `custom_bpp` or `COMPRESSION_BPP`: Maximum 0.3, Social 0.15, Web 0.08, Potato 0.04. - `optimize_filesize`, GIF and cursor-only exports are not handled locally and go to the server. **Constants:** | Constant | Safari | Other browsers | |---|---|---| | `HOLDS_QUALITY_FRAMES` | true | false | | `KEYFRAME_SECONDS` | 5 | 2 | | `BITRATE_SCALE` | 1.35 | 1 | **Config, literal:** ```ts const bitrate = Math.round(exportBitrate(width, height, job.fps, job.bitsPerPixel) * BITRATE_SCALE); // guarded by mediabunny canEncodeVideo("avc", { width, height, bitrate }) const encoderConfig: VideoEncoderConfig = { codec: avcCodecString(width, height, bitrate), width, height, bitrate, framerate: job.fps, latencyMode: HOLDS_QUALITY_FRAMES ? "realtime" : "quality", hardwareAcceleration: "prefer-hardware", alpha: "discard", avc: { format: "avc" }, }; ``` **Codec string** (`avcCodecString`): `avc1.6400LL`, where LL is the first entry of `AVC_LEVELS` with `ceil(W/16)*ceil(H/16) <= maxMacroblocks` and `bitrate <= maxBitrate`. The table mirrors mediabunny. **Frames and keyframes:** - `VideoFrame` timestamp is `trunc(frame/fps*1e6)`, and duration is `trunc(1e6/fps)`. This is CFR. - A keyframe is requested when `floor(frame/fps/KEYFRAME_SECONDS)` changes: every 2 s, or every 5 s on Safari. - Up to 4 frames are queued in the encoder. **Container and audio:** - `Mp4OutputFormat({ fastStart: file ? "reserve" : "in-memory" })`. Either way the moov is at the front. - Packets are re-stamped `frame/fps`. - Audio: 48 kHz stereo, AAC at `EXPORT_AUDIO_BITRATE` = 320,000 if `canEncodeAudio`, else Opus. **Fallback:** - An error before the first progress message becomes `BrowserLocalExportUnavailable`. - `tauri-bridge.ts` lines 248-271 then switches to the server export (section 8.6). - In headless Linux Chrome, `isConfigSupported` with `prefer-hardware` returns unsupported (verified), so the PR as written falls back to the server there. **Worked values (non-Safari, Maximum 0.3):** | | 1280x720@30 | 1920x1080@30 | 1920x1080@60 | 3840x2160@30 | |---|---|---|---|---| | Bitrate | 8,294,400 | 18,662,400 | 29,859,840 | 74,649,600 | | Codec | `avc1.64001F` | `avc1.640028` | `avc1.640029` | `avc1.640033` | Codecs at Social, Web and Potato: `64001F`, `640028`, `640028`, `640033`. Safari at Maximum (×1.35): 11,197,440 / 25,194,240 / 40,310,784 / 100,776,960. K is 60 at 30 fps and 120 at 60 fps (Safari: 150 at 30 fps). **Reproduction on Linux** (Playwright, Google Chrome; substitute `hardwareAcceleration`): ```js VideoEncoder.configure({ codec: "avc1.640028", width: 1920, height: 1080, bitrate: 18662400, framerate: 30, latencyMode: "quality", hardwareAcceleration: "no-preference", // production: "prefer-hardware" alpha: "discard", avc: { format: "avc" }, }) // encode(frame, { keyFrame: Math.floor(i/30/2) !== Math.floor((i-1)/30/2) || i === 0 }) ``` **Observed Chrome 148 software path on Linux (verified, 1280x720, `avc1.640028` requested, 4,976,640 bit/s):** - avcC `profile_idc` 100 with constraint byte `0x0C` (Constrained High). - `level_idc` 31, not the requested 0x28. - No B-frames. - Requested keyframes were honoured, with extra encoder-inserted IDRs on scene changes. ### 8.5 BR-WC-AUDIO2VIDEO-PR: audio-only to video **Location:** `apps/web/lib/audio-to-video.ts`. ```js // 1280x720 canvas new Mp4OutputFormat({ fastStart: "in-memory" }) new CanvasSource(canvas, { codec: "avc", bitrate: 150_000, keyFrameInterval: 60, latencyMode: "realtime" }) // frameRate 1 // audio: AAC (Opus if unsupported) at 160_000 ``` - Codec: `avc1.64001F` (mediabunny). - Keyframe interval: 60 s, which is 60 frames at 1 fps. Reproduction: ```js VideoEncoder.configure({ codec: "avc1.64001F", width: 1280, height: 720, bitrate: 150000, framerate: 1, latencyMode: "realtime", hardwareAcceleration: "no-preference", avc: { format: "avc" }, }) ``` Set `keyFrame` every 60th frame. ### 8.6 Server export and web Save (PR) **Server export:** - `apps/media-server/src/lib/editor-exports.ts` validates settings with zod (fps 1-60, x ≤ 3840, y ≤ 2160, `custom_bpp` ≤ 1, `optimize_filesize` optional). - It spawns the native `export` binary (`crates/export/src/bin/web-editor.rs` `export()`, lines 512-566). - That binary clamps fps to `1..=60`, caps size at 3840x2160, sets `force_ffmpeg_decoder = true`, and calls `Mp4ExportSettings.export`. - So it is identical to section 5: on a Linux server, DESK-EXPORT-X264-* by default, DESK-EXPORT-CRF-* with optimize file size, and NVENC p5 if `/dev/nvidiactl` exists. **Web Save** (`apps/web/lib/render-farm-start.ts`): | Setting | Value | |---|---| | Resolution | `SAVE_RESOLUTION = [1920, 1080]` | | fps | `clamp(round(fps ?? 30), 24, 60)` | | Format | Mp4 | | Compression | Maximum | | `custom_bpp` | null | - Rendered by RF-RENDER-NVENC in production. - At 30 fps: B = 18,662,400, M = 27,993,600, K = 60. At 60 fps: B = 29,859,841, K = 120. - The fallback worker save uses the same `workerSaveSettings` without `optimize_filesize`, so it has no faststart. ### 8.7 PR-PREVIEW-X264: live editor preview stream **Locations:** - `crates/editor/src/bin/web-editor-service/preview_h264.rs` - `web-editor-service.rs` (default preview_fps 60 at line 1255; bpp 18/100 at line 990; `{"bitrate":"low"}` gives 4/100 and `{"bitrate":"high"}` gives 18/100, lines 1053-1056) **Configuration:** - `VideoInfo`: RGBA, `time_base 1/1_000_000`, `frame_rate fps/1`. - Validation: fps `1..=60`, bpp `0.04..=0.18`. - Builder: `.with_bpp(bpp).with_encoder_priority_override(&["libx264"]).build_standalone()`. - Preset is the builder default `Ultrafast` in non-export mode, giving `preset=ultrafast`, `tune=zerolatency`, `g=K`, `keyint_min=K`. **Encoder settings:** | Setting | Value | |---|---| | Rate control | ABR at B, maxrate ignored | | GOP | 2*fps, so 120 at the default 60 fps | | Profile | Constrained Baseline (verified) | | Colour | BT.709 limited, from RGBA via swscale | | Output | Raw AVC packets with avcC description, sent to the browser decoder. There is no container. | **Worked B:** | Resolution | bpp 0.18 | bpp 0.04 | |---|---|---| | 1280x720@60 | 7,962,624 | 1,769,471 | | 1920x1080@60 | 17,915,904 | 3,981,311 | **Reproduction on Linux** (verified at 720p30): ``` ffmpeg -y -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv \ -c:v libx264 -preset ultrafast -tune zerolatency -threads N -b:v B -maxrate M -g K -keyint_min K \ -pix_fmt yuv420p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \ -an -f h264 preview.h264 ``` The production defaults are FPS=60, K=120 and bpp 0.18. ### 8.8 PR media server and render farm changes **`media-video.ts` (PR):** - `getOrientedLimits` swaps the max width and height for portrait sources, so the limit becomes 1080x1920. - Level selection uses the long and short edge. - `AUDIO_RUN_ARGS = ["-chunk_duration", "1000000"]` is appended after `+faststart` when audio is present, and in `muxMediaTracksToMp4`. - Video encoder arguments are unchanged. **Render farm (PR):** - `transcode.ts` only adds `downloadSource`. - `worker.ts` line 721 keeps `RF_TRANSCODE_ENCODER ?? "h264_nvenc"`. - `protocol.ts` adds a `CursorTask` that runs the external `cap-cursor-service` binary. Its encoding settings are not in this repo. --- ## 9. Running browser surfaces in headless Chrome with Playwright 1. Use Google Chrome, not Playwright's bundled Chromium, which lacks proprietary H.264/MP4 support: ```js chromium.launch({ executablePath: "/usr/local/bin/google-chrome", args: [...] }) ``` `channel: "chrome"` also works. 2. Feed the source frames: - **MediaRecorder:** convert the raw input to y4m and use it as the fake camera: ``` ffmpeg -f rawvideo -pix_fmt yuv420p -s WxH -r FPS -i in.yuv -f yuv4mpegpipe in.y4m ``` Launch with `--use-fake-device-for-media-stream --use-fake-ui-for-media-stream --use-file-for-fake-video-capture=/abs/in.y4m`. Then call `getUserMedia({ video: { width: { exact: W }, height: { exact: H }, frameRate: { exact: FPS } } })`. The fake device loops the file. Content type changes only the bitrate tier, so the same track works for the screen, camera and overlay variants. - **WebCodecs:** serve `in.yuv` over HTTP. Slice `W*H*3/2` bytes per frame and build: ```js new VideoFrame(buf, { format: "I420", codedWidth: W, codedHeight: H, timestamp: Math.trunc(i/FPS*1e6), duration: Math.trunc(1e6/FPS) }) ``` 3. Replace `hardwareAcceleration: "prefer-hardware"` with `"no-preference"` or `"prefer-software"`. Headless Linux Chrome 148 reports `prefer-hardware` unsupported, while `no-preference` and `prefer-software` (OpenH264) work. This changes the encoder compared with production on Windows/macOS. 4. Linux Chrome supports neither AAC in MediaRecorder nor AAC in `AudioEncoder`. Use the `,opus` MIME variants, or video-only. 5. Avoid `--dump-dom --virtual-time-budget` for encoder probes. Virtual time stalls the encoders. Post results from the page to a local HTTP endpoint instead (used here: about 8 s per probe). --- ## 10. Ambiguities and assumptions - **Media-server ffmpeg version:** `oven/bun:1.4.0` plus apt `ffmpeg` is unpinned. It is probably Debian's 5.1.x but not verified. Verification here used 6.1.1. The x264 presets and the `fps_mode auto` choice of CFR for MP4 are expected to match. - **Media-server output frame rate:** for VFR WebM input, the output rate is whatever ffmpeg guesses (`r_frame_rate`, often the stream's nominal 30/1 or a 1000/1 time base fallback). The repro uses the raw input rate. - **VideoToolbox GOP of 12** comes from the FFmpeg 7.1 source (AVCodecContext `gop_size` default, `vt_defaults`). It is not observed on a Mac. The FFmpeg version linked into the desktop app was not checked. - **NVENC, QSV, AMF, Media Foundation and AVFoundation:** B-frames, profile, level, VBV and lookahead not set by Cap come from encoder or preset defaults. These were not observed. - **Rust f32 model:** f32-to-f64 rounding makes some values 1-3 bit/s off the browser model. Benchmarks should use the Rust values for Rust surfaces. - **getDisplayMedia and getUserMedia** use `ideal` constraints, so actual capture size and fps may differ. The PR bitrate uses `track.getSettings()`, so it follows the actual size. - **Chrome MediaRecorder** chooses its own H.264 level (it reported `avc1.640020` for 720p) and inserts scene-change IDRs. On Windows/macOS Chrome uses platform hardware encoders, whose output will differ from Linux OpenH264. - **Chromium default WebCodecs bitrate** comes from the Chromium source. The probe's achieved rate on synthetic content was dominated by scene-change IDRs, so it is not evidence either way. - **The `video/mp4` MIME without codecs** (web tools) leaves the codec choice to the browser. - **Instant-mode output size** on desktop is clamped to fit within (max, max*9/16). The free/Pro limit comes from the account, which is outside the encoder code. - **`cap-cursor-service`** (PR cursor-only render) is an external binary whose encoding is unknown. - **libx264 dictionary keys:** the "ignored" list is based on FFmpeg option names and on observed `ref=1` in the output. A different FFmpeg build linked into the desktop app could accept other aliases. This was not checked. --- ## 11. Summary table | surface | encoder | rate control | GOP | preset/tune | profile/level | container | repro command id | |---|---|---|---|---|---|---|---| | Media server processVideo (non-H.264, >1080p or level too high) | libx264 | CRF 23, no cap | x264 default 250/25, scenecut; `-bf 0` for VP8/VP9 source, else 3 B-frames | medium / none | High / forced 4.2 (5.1, 5.2 above 2048x1088) | MP4 `+faststart`, CFR (auto), AAC 128k | MS-TRANSCODE, MS-TRANSCODE-VPX | | Media server level fix (dormant) | copy + `h264_metadata` | n/a | source | n/a | level rewritten | MP4 `+faststart` | MS-LEVELFIX | | Media server edit render | libx264 | CRF 18 | 250/25, 3 B-frames | fast / none | High / auto | MP4 `+faststart`, CFR via fps filter, AAC 160k | MS-EDIT | | Web Loom download fallback | libx264 | CRF 23 (default) | 250/25, 3 B-frames | veryfast / none | High / auto | MP4 `+faststart`, AAC 128k | WEB-LOOM-FFMPEG | | Render farm render (production) | h264_nvenc | VBR B, max 1.5B (bpp model, default Maximum 0.3) | 2*fps, forced IDR, bf 0 | p1 / hq, spatial+temporal AQ | encoder default | Custom MP4 (moov first) or fMP4 HLS 2 s, AAC 320k | RF-RENDER-NVENC | | Render farm render (override) | libx264 | ABR B, maxrate ignored | 2*fps, min fps+1, bf 0 | veryfast (ref 1, rc-lookahead 10) | High / auto | as above | RF-RENDER-X264 | | Render farm source transcode (production) | h264_nvenc | VBR CQ 19, b:v 0 | forced IDR every 1 s, bf 0 | p1 / none | encoder default | MP4 `+faststart`, VFR passthrough, no audio | RF-TRANSCODE-NVENC | | Render farm source transcode (override) | libx264 | CRF 18 | forced every 1 s plus 250/25, bf 0 | veryfast / none | High / auto | MP4 `+faststart`, VFR passthrough | RF-TRANSCODE-X264 | | Desktop recording Linux (Balanced/Compat/Instant) | libx264 (h264_nvenc p4/ll if `/dev/nvidiactl`) | ABR B at bpp 0.3, maxrate ignored | 2*fps, min fps+1, scenecut 0, bf 0 | ultrafast / zerolatency | Constrained Baseline / auto (3.2 at 720p30) | DASH fMP4 2 s + HLS playlists, VFR, AAC 320k | DESK-REC-X264 | | Desktop recording Linux Ultra | libx264 | ABR B at bpp 0.3 | 2*fps, min fps+1, bf 0 | veryfast / zerolatency | auto | DASH fMP4 2 s | DESK-REC-X264-ULTRA | | Desktop recording macOS fragmented (default) | h264_videotoolbox (libx264 fallback) | ABR B (bpp 0.3 / 1.0 Ultra / 0.15 Compat or Instant), DataRateLimits 1.5B per s | 12 frames (not set), no B-frames | realtime, prio_speed | Main / AutoLevel | DASH fMP4 2 s, VFR | DESK-REC-VT (not runnable) | | Desktop recording macOS non-fragmented and camera | AVFoundation | AverageBitRate (Balanced/Ultra/Compat/Instant formulas) | floor(0.75*fps); reordering only Ultra | n/a | AVFoundation default | MP4 (AVAssetWriter) | DESK-REC-AVF (not runnable) | | Desktop recording Windows fragmented (default) | nvenc/mf/qsv/amf, then libx264 | VBR B (bpp 0.3 / 1.0 Ultra / 0.15 Instant), max 1.5B | 2*fps | NVENC p4/ll; QSV faster; AMF balanced vbr_latency; MF scenario 4 | encoder default | DASH fMP4 2 s | DESK-REC-WIN-HW (not runnable) | | Desktop recording Windows non-fragmented | Media Foundation | AVG_BITRATE, fps fixed 30, multiplier 0.15 (0.3 Ultra) | MFT default | MFT default | MFT default | MP4, CFR 30 | DESK-REC-WINMF (not runnable) | | Desktop export (default, Linux or software) | libx264 | ABR B: 0.3 / 0.15 / 0.08 / 0.04 | 2*fps, min fps+1, bf 0 | veryfast (ref 1, rc-lookahead 10, trellis 0) | High / auto | MP4, moov at end, CFR, AAC 320k | DESK-EXPORT-X264-{Maximum,Social,Web,Potato} | | Desktop export, Optimize file size | libx264 | CRF 24 / 28 / 32 / 36 | 2*fps, min fps+1, 3 B-frames | slow / none | High / auto | MP4 `+faststart` | DESK-EXPORT-CRF-{24,28,32,36} | | Desktop export macOS | h264_videotoolbox | ABR B, DataRateLimits 1.5B | 12 frames (not set) | realtime false, allow_sw 0 | Main / AutoLevel | MP4, moov at end | DESK-EXPORT-VT (not runnable) | | Desktop export Windows/Linux NVIDIA | h264_nvenc | VBR B, max 1.5B | 2*fps | p5 / hq, b_ref_mode middle, AQ | encoder default | MP4, moov at end | DESK-EXPORT-NVENC (not runnable) | | Desktop export QSV / AMF / MF | h264_qsv / h264_amf / h264_mf | VBR(-peak) B, max 1.5B | 2*fps | medium la 20 / quality vbr_peak / scenario 0 quality 0 | encoder default | MP4, moov at end | DESK-EXPORT-QSV, -AMF, -MF (not runnable) | | Web recorder main, Chrome | MediaRecorder VP9 (H.264 made by MS-TRANSCODE-VPX) | browser default 2.5 Mbps | 2 s requested | n/a | VP9 | WebM streaming, 1 s slices | BR-MR-MAIN | | Web recorder main, Safari/Firefox | MediaRecorder H.264 | browser default | 2 s requested | n/a | `avc1.42E01E` / `4d401e` requested | MP4 buffered, 200 ms slices | BR-MR-MAIN-MP4 | | Chrome extension main | MediaRecorder VP9 | browser default | browser default | n/a | VP9 | WebM, 1 s slices | BR-MR-EXT-MAIN | | Web tools (speed, trim) | MediaRecorder | 5,000,000 | browser default | n/a | browser choice for `video/mp4` | MP4 or WebM | BR-MR-TOOLS | | Comment media / format converter | WebCodecs via Remotion | none (Chromium default formula; Safari 3M) | 40 frames | prefer-hardware, then software | 0x64, level table; clamp to `64002A/640033/640034` for comments | MP4 (AAC) | BR-WC-REMOTION | | Loom browser conversion | WebCodecs via mediabunny | QUALITY_HIGH (2.778M / 6M / 22.393M) | 2 s | prefer-hardware, latency quality | `6400LL` from mediabunny table | MP4 moov at end, AAC 128k | BR-WC-MEDIABUNNY-LOOM | | PR web recorder screen/camera/overlay | MediaRecorder H.264 | tier bit/s × 1.5 above 40 fps × 1.6 high × 1.3 VP8 | 2 s requested | n/a | `avc1.64002A` / `640033` / `640034` (Chrome picks actual level) | fragmented MP4 streaming, 1 s slices | BR-MR-PR-SCREEN, -CAMERA, -OVERLAY | | PR editor clip recorder | MediaRecorder H.264 | browser default | browser default | n/a | `avc1.64002A` | fragmented MP4, 1 s slices | BR-MR-CLIP-PR | | PR browser export | WebCodecs | bpp model × (Safari 1.35) | 2 s (Safari 5 s) | prefer-hardware, latency quality (Safari realtime) | `avc1.6400LL` from table | MP4 moov first, AAC 320k or Opus | BR-WC-EXPORT-PR | | PR audio-to-video | WebCodecs via mediabunny | 150,000 | 60 frames at 1 fps | latency realtime | `avc1.64001F` | MP4 moov first, audio 160k | BR-WC-AUDIO2VIDEO-PR | | PR editor preview stream | libx264 | ABR bpp 0.18 (low 0.04) | 2*fps (120 at 60 fps), min fps+1 | ultrafast / zerolatency | Constrained Baseline / auto | raw AVC packets | PR-PREVIEW-X264 | | PR server export | as desktop export | as desktop export | as desktop export | as desktop export | as desktop export | MP4 | DESK-EXPORT-X264-*, DESK-EXPORT-CRF-* | | PR web Save | as render farm render, 1920x1080 Maximum, fps 24-60 | VBR B, max 1.5B | 2*fps | p1 / hq | encoder default | Custom MP4 or fMP4 HLS | RF-RENDER-NVENC |