mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat: add browser GPU render mode (#571)
## Problem HyperFrames already had `--gpu`, but that flag only controlled FFmpeg hardware encoding. The browser capture path still forced Chrome/WebGL through SwiftShader software GL via `--use-angle=swiftshader`, so WebGL-heavy local renders could leave the biggest bottleneck on the CPU path. That made the existing flag naming easy to misread: `--gpu` sounded like it accelerated the whole render, but it did not change the browser frame-capture backend. ## What this fixes - Enables host browser GPU acceleration automatically for local CLI renders. - Adds `--no-browser-gpu` as the local opt-out for software Chrome/WebGL capture. - Keeps `--browser-gpu` as an explicit local browser-GPU request. - Adds `browserGpuMode: "software" | "hardware"` to engine config, with `PRODUCER_BROWSER_GPU_MODE` env support for lower-level producer users. - Keeps Docker browser capture on the deterministic software path. - Maps hardware browser GPU mode to platform-native Chrome backends: - macOS: Metal-backed ANGLE - Windows: D3D11-backed ANGLE - Linux: EGL - Blocks explicit `--browser-gpu --docker` with a clear error because Docker browser GPU passthrough is not cross-platform. - Clarifies docs so `--gpu` means FFmpeg encoder GPU and browser GPU means Chrome/WebGL capture GPU. - Keeps encoder backend selection auto-detected from FFmpeg capabilities: - NVIDIA: NVENC - macOS: VideoToolbox - Linux: VAAPI - Intel: QSV ## Why two flags There are two separate GPU surfaces in the render pipeline: 1. Browser GPU controls Chrome frame capture. - Affects WebGL, canvas, CSS rendering, compositing, and screenshot capture inside the browser. - This is enabled automatically for local CLI renders. - Use `--no-browser-gpu` when you want the software browser baseline. 2. `--gpu` controls FFmpeg video encoding. - Affects the final encode step after frames have already been captured. - The concrete encoder is auto-detected from the host FFmpeg build and hardware. - It can be faster for some machines/codecs, but it is not equivalent to browser rendering acceleration. The controls stay independent because users may want: - `hyperframes render` for the fast local default with browser GPU capture. - `hyperframes render --no-browser-gpu` for the software-browser local baseline. - `hyperframes render --gpu` for browser GPU capture plus hardware FFmpeg encoding. - `hyperframes render --no-browser-gpu --gpu` for software browser capture plus hardware FFmpeg encoding. - `hyperframes render --docker` for deterministic browser capture. ## Why `--gpu` does not imply browser GPU Keeping `--gpu` scoped to FFmpeg encoding avoids a semantic break and keeps the risk profile explicit: - `--gpu` already means encoder acceleration. Expanding it to also change Chrome capture would silently alter behavior for users who only wanted hardware encoding. - Browser GPU and encoder GPU have different portability. Encoder GPU can work in Docker when the host exposes the right devices; browser GPU passthrough is not cross-platform, so this PR intentionally blocks explicit `--browser-gpu --docker`. - The Apple presentation benchmark shows why the controls should stay separate: browser GPU capture was the useful improvement, while macOS VideoToolbox via `--gpu` was slower and produced larger output for this `standard` H.264 run. If HyperFrames later wants a single umbrella acceleration control, it should be explicit, for example `--acceleration browser|encoder|all` or `--gpu=browser|encoder|all`, rather than changing the meaning of the existing boolean `--gpu`. ## Root cause `buildChromeArgs()` always injected `--use-gl=angle --use-angle=swiftshader`. `disableGpu` only appended `--disable-gpu`; it did not provide a hardware-GPU mode. That made the public `--gpu` flag look broader than it was, because render capture stayed software-backed even when encoder GPU was requested. ## Verification ### Local checks - `bun install` - `bun run build:hyperframes-runtime` - `bun run --filter @hyperframes/engine test src/config.test.ts src/services/browserManager.test.ts` - `bun run --filter @hyperframes/cli test src/utils/dockerRunArgs.test.ts src/commands/render.test.ts` - `bun run --filter @hyperframes/cli typecheck` - `bun run --filter @hyperframes/engine typecheck` - `bun run --filter @hyperframes/producer typecheck` - `cd packages/producer && bunx vitest run src/services/renderOrchestrator.test.ts` - `bunx oxlint packages/cli/src/commands/render.ts packages/cli/src/commands/render.test.ts packages/cli/src/utils/dockerRunArgs.ts packages/cli/src/utils/dockerRunArgs.test.ts packages/engine/src/config.ts packages/engine/src/config.test.ts packages/engine/src/services/browserManager.ts packages/engine/src/services/browserManager.test.ts packages/producer/src/services/renderOrchestrator.test.ts` - `bunx oxfmt --check ...` on changed source/docs files - `git diff --check` - `bun packages/cli/src/cli.ts render --help | rg -n "browser-gpu|no-browser-gpu|GPU"` - `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --output /tmp/hf-auto-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict` - Render plan prints `GPU: browser GPU (auto)`. - `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --no-browser-gpu --output /tmp/hf-software-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict` - Render plan does not print browser GPU. - `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --docker --browser-gpu --output /tmp/should-not-render.mp4` - Exits 1 with `Browser GPU is local-only`. - `buildDockerRunArgs()` regression coverage asserts Docker container args include `--no-browser-gpu`, preventing nested container renders from re-enabling browser GPU through the local CLI default. - `resolveBrowserGpuForCli()` regression coverage asserts `PRODUCER_BROWSER_GPU_MODE=software` opts out when no CLI browser-GPU flag is supplied, while explicit `--browser-gpu` / `--no-browser-gpu` still win. - `ffmpeg -v error -i /tmp/hf-auto-browser-gpu-smoke.mp4 -f null -` - `ffmpeg -v error -i /tmp/hf-software-browser-gpu-smoke.mp4 -f null -` - `ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of json /tmp/hf-browser-gpu-smoke.mp4` -> H.264, 1920x1080, 24fps, 5.0s ### Apple presentation benchmark Rendered `/Users/miguel07code/Downloads/apple-presentation.zip` as supplied after extracting to `/tmp/hf-apple-profile/apple-presentation`. Fixed settings: - 1920x1080 - 30fps - `standard` quality - 4240 frames - 141.32s duration - 8-worker cap; render auto-calibration used 6 capture workers - macOS host detected FFmpeg GPU encoder: `videotoolbox` | Mode | Equivalent flags after this PR | Wall time | vs software-browser baseline | Speed | Capture | Encode | Output | | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | | Software browser + CPU encode | `--no-browser-gpu` | 120.77s | baseline | 1.17x | 97.87s | 10.04s | 8.38MB | | Browser GPU + CPU encode | default local render | 70.10s | 42.0% faster | 2.02x | 50.72s | 9.91s | 8.39MB | | Software browser + encoder GPU | `--no-browser-gpu --gpu` | 133.16s | 10.3% slower | 1.06x | 103.58s | 18.31s | 25.43MB | | Browser GPU + encoder GPU | `--gpu` | 74.12s | 38.6% faster | 1.91x | 46.69s | 17.93s | 25.45MB | Result: browser GPU capture is the meaningful improvement for this WebGL/browser-capture-heavy presentation. VideoToolbox encoding was slower and produced larger files for this current `standard` H.264 path, so `--gpu` should stay separate and opt-in. Why `--gpu` plus browser GPU was slower than browser GPU alone: the combined run captured about 4.0s faster than browser GPU alone, but VideoToolbox encoding was about 8.0s slower than CPU x264 encoding, so the encode loss outweighed the capture gain. ### VideoToolbox flag check I also isolated the encode stage against the already-captured Apple frames to check whether macOS GPU encoding only needed special flags. `ffmpeg -h encoder=h264_videotoolbox` does not expose a CRF/CQ-style quality option like x264. It exposes bitrate-oriented and VideoToolbox-specific options such as `-b:v`, `-realtime`, `-profile`, `-coder`, `-prio_speed`, `-power_efficient`, and `-allow_sw`. That means our current `-q:v` mapping is not equivalent to x264 CRF and can produce very different bitrate/size behavior. Measured full-frame encode variants on this host: | VideoToolbox variant | Encode wall time | Output size | Bitrate | | --- | ---: | ---: | ---: | | Current `-q:v 64 -allow_sw 1` | 18.76s | 25.31MB | 1.43 Mbps | | Current without `-allow_sw 1` | 18.21s | 25.31MB | 1.43 Mbps | | `-b:v 500k -maxrate 750k -bufsize 1000k -profile high -coder cabac -realtime 1 -prio_speed 1 -power_efficient 0` | 20.58s | 7.42MB | 0.42 Mbps | | Same with `-b:v 1500k` | 20.84s | 16.70MB | 0.95 Mbps | | `-b:v 500k -profile baseline -coder cavlc -realtime 1 -prio_speed 1 -power_efficient 0` | 18.11s | 8.94MB | 0.51 Mbps | Conclusion: VideoToolbox can be made size/bitrate-predictable with explicit `--video-bitrate`, but the tested speed-oriented flags did not make it faster than CPU x264 wall time for this render. That reinforces keeping `--gpu` encoder acceleration explicit and separate from browser GPU capture. Artifacts from the local benchmark: - `/tmp/hf-apple-profile/results/cpu.mp4` - `/tmp/hf-apple-profile/results/browser-gpu.mp4` - `/tmp/hf-apple-profile/results/encoder-gpu.mp4` - `/tmp/hf-apple-profile/results/full-gpu.mp4` - `/tmp/hf-apple-profile/results/summary.json` All four benchmark MP4s completed `ffprobe` and full `ffmpeg -f null` decode checks. ### Pixel comparison Compared decoded MP4 output between software-browser and browser-GPU renders: - Apple presentation: - 4240 frames compared - 636 exact matching decoded frame hashes - 3604 different decoded frame hashes - Average PSNR: 57.79 dB - `css-spinner-render-compat` clean fixture: - 120 frames compared - 0 exact matching decoded frame hashes - Average PSNR: 61.57 dB Interpretation: browser GPU output is not strict hash/pixel-identical to the software-browser path after lossy H.264 encode, but the measured deltas are visually tiny. Above 50 dB PSNR is typically visually indistinguishable for normal video review. Use `--no-browser-gpu` or Docker when strict cross-run/cross-machine reproducibility matters more than local speed. ### Browser verification - Started HyperFrames Studio preview for `packages/producer/tests/css-spinner-render-compat/src`. - Used `agent-browser` to open `http://localhost:5191#project/src` and verify the composition loaded in Studio. - Screenshots: - `/tmp/hf-gpu-browser-proof/preview-loaded.png` - `/tmp/hf-gpu-browser-proof/preview-playing.png` - `/tmp/hf-gpu-browser-proof/preview-frame-60.png` - Agent-browser recordings: - `/tmp/hf-gpu-browser-proof/preview-playback.webm` - `/tmp/hf-gpu-browser-proof/preview-seek.webm` ## Notes - Browser GPU is enabled automatically for local CLI renders and disabled in Docker. - `--no-browser-gpu` is the opt-out for software Chrome/WebGL capture. - `--gpu` remains encoder-only and opt-in. - The Apple presentation zip has existing lint errors around unmanaged nested videos and imperative media `play()` calls. The benchmark still compares the same supplied source across modes, but it should not be treated as a clean deterministic-composition fixture.
This commit is contained in:
@@ -65,7 +65,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
|
|||||||
|
|
||||||
**Pros:**
|
**Pros:**
|
||||||
- Fast startup, no container overhead
|
- Fast startup, no container overhead
|
||||||
- Uses your system GPU for hardware-accelerated encoding (with `--gpu`)
|
- Can use your system GPU for Chrome/WebGL capture by default
|
||||||
|
- Can use your system GPU for hardware-accelerated encoding (with `--gpu`)
|
||||||
- Best for iterative development
|
- Best for iterative development
|
||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
@@ -90,7 +91,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
|
|||||||
|
|
||||||
**Cons:**
|
**Cons:**
|
||||||
- Slower startup due to container initialization
|
- Slower startup due to container initialization
|
||||||
- No GPU acceleration inside the container
|
- Browser capture stays on the deterministic software-GL path
|
||||||
|
- GPU encoding requires Docker host GPU passthrough and is not cross-platform on Docker Desktop
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Docker mode uses `chrome-headless-shell` with [BeginFrame](/concepts/determinism#how-it-works) control for frame-perfect, deterministic capture.
|
Docker mode uses `chrome-headless-shell` with [BeginFrame](/concepts/determinism#how-it-works) control for frame-perfect, deterministic capture.
|
||||||
@@ -121,7 +123,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
|
|||||||
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate encoding. Cannot combine with `--crf` |
|
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate encoding. Cannot combine with `--crf` |
|
||||||
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
|
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
|
||||||
| `--max-concurrent-renders` | 1-10 | 2 | Max simultaneous renders via the producer server (see [Concurrent Renders](#concurrent-renders) below) |
|
| `--max-concurrent-renders` | 1-10 | 2 | Max simultaneous renders via the producer server (see [Concurrent Renders](#concurrent-renders) below) |
|
||||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI) |
|
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
|
||||||
|
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
|
||||||
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected (MP4 only). See [HDR Rendering](/guides/hdr) |
|
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected (MP4 only). See [HDR Rendering](/guides/hdr) |
|
||||||
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
||||||
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
||||||
@@ -149,6 +152,26 @@ npx hyperframes render --video-bitrate 10M --output controlled.mp4
|
|||||||
|
|
||||||
**Tip**: The default `standard` preset (CRF 18) is visually lossless at 1080p — most people cannot distinguish it from the source. Use `--quality draft` for faster iteration, or `--quality high` / `--crf 10` when file size is no concern.
|
**Tip**: The default `standard` preset (CRF 18) is visually lossless at 1080p — most people cannot distinguish it from the source. Use `--quality draft` for faster iteration, or `--quality high` / `--crf 10` when file size is no concern.
|
||||||
|
|
||||||
|
## GPU Acceleration
|
||||||
|
|
||||||
|
Hyperframes has two separate GPU acceleration surfaces:
|
||||||
|
|
||||||
|
- `--gpu` uses a hardware video encoder in FFmpeg when one is available. Supported backends include VideoToolbox on macOS, NVENC on NVIDIA systems, VAAPI on Linux, and Intel QSV on supported Windows/Linux hosts.
|
||||||
|
- Browser GPU uses the host GPU for local Chrome/WebGL capture. It is enabled automatically for local renders and disabled in Docker. Use `--no-browser-gpu` to opt out.
|
||||||
|
|
||||||
|
```bash Terminal
|
||||||
|
# Add hardware FFmpeg encoding to the default local browser-GPU render
|
||||||
|
npx hyperframes render --gpu --output encoded-fast.mp4
|
||||||
|
|
||||||
|
# Opt out of hardware Chrome/WebGL capture
|
||||||
|
npx hyperframes render --no-browser-gpu --output software-browser.mp4
|
||||||
|
|
||||||
|
# Use browser GPU plus hardware FFmpeg encoding
|
||||||
|
npx hyperframes render --gpu --output gpu.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser GPU capture is local-mode only. It maps to platform-native Chrome GPU backends: Metal on macOS, D3D11 on Windows, and EGL on Linux. Use `--no-browser-gpu` or Docker mode when exact cross-machine reproducibility matters more than local render speed.
|
||||||
|
|
||||||
## Workers
|
## Workers
|
||||||
|
|
||||||
Each render worker launches a **separate Chrome browser process** to capture frames in parallel. More workers can speed up rendering, but each one consumes ~256 MB of RAM and significant CPU.
|
Each render worker launches a **separate Chrome browser process** to capture frames in parallel. More workers can speed up rendering, but each one consumes ~256 MB of RAM and significant CPU.
|
||||||
|
|||||||
@@ -128,9 +128,10 @@ If your issue is about a specific coding mistake (animations not working, video
|
|||||||
|
|
||||||
1. Use `--quality draft` during development for faster encoding
|
1. Use `--quality draft` during development for faster encoding
|
||||||
2. Run `npx hyperframes benchmark` to find the optimal worker count for your system
|
2. Run `npx hyperframes benchmark` to find the optimal worker count for your system
|
||||||
3. Use `--gpu` for hardware-accelerated encoding (local mode only)
|
3. Local Chrome/WebGL GPU capture is enabled automatically; compare with `--no-browser-gpu` if troubleshooting
|
||||||
4. Reduce `--fps` to 24 if 30fps is not needed
|
4. Use `--gpu` for hardware-accelerated encoding (local mode only)
|
||||||
5. Check that your composition does not have unnecessary elements or overly complex animations
|
5. Reduce `--fps` to 24 if 30fps is not needed
|
||||||
|
6. Check that your composition does not have unnecessary elements or overly complex animations
|
||||||
|
|
||||||
See [Rendering: Options](/guides/rendering#options) for all available flags.
|
See [Rendering: Options](/guides/rendering#options) for all available flags.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -530,6 +530,12 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
|
|||||||
|
|
||||||
# With options
|
# With options
|
||||||
npx hyperframes render --output output.mp4 --fps 60 --quality high
|
npx hyperframes render --output output.mp4 --fps 60 --quality high
|
||||||
|
|
||||||
|
# Opt out of local browser GPU capture
|
||||||
|
npx hyperframes render --no-browser-gpu --output cpu-browser.mp4
|
||||||
|
|
||||||
|
# Add hardware FFmpeg encoding
|
||||||
|
npx hyperframes render --gpu --output gpu.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Values | Default | Description |
|
| Flag | Values | Default | Description |
|
||||||
@@ -543,7 +549,8 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
|
|||||||
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
|
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
|
||||||
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
||||||
| `--workers` | 1-8 | 4 | Parallel render workers |
|
| `--workers` | 1-8 | 4 | Parallel render workers |
|
||||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI) |
|
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
|
||||||
|
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
|
||||||
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
||||||
| `--quiet` | — | off | Suppress verbose output |
|
| `--quiet` | — | off | Suppress verbose output |
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ await applyFaststart(inputPath, outputPath);
|
|||||||
|
|
||||||
// Detect GPU encoding support
|
// Detect GPU encoding support
|
||||||
const gpu = await detectGpuEncoder();
|
const gpu = await detectGpuEncoder();
|
||||||
// gpu: "nvenc" | "videotoolbox" | "vaapi" | null
|
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null
|
||||||
```
|
```
|
||||||
|
|
||||||
#### WebM with VP9 Alpha
|
#### WebM with VP9 Alpha
|
||||||
|
|||||||
@@ -213,18 +213,31 @@ npx hyperframes render --docker --output output.mp4
|
|||||||
|
|
||||||
The producer supports hardware-accelerated encoding for faster renders:
|
The producer supports hardware-accelerated encoding for faster renders:
|
||||||
|
|
||||||
| Platform | Encoder | Flag |
|
| Platform | Encoder | Selection |
|
||||||
|----------|---------|------|
|
|----------|---------|-----------|
|
||||||
| NVIDIA | NVENC | Auto-detected |
|
| NVIDIA | NVENC | Auto-detected |
|
||||||
| macOS | VideoToolbox | Auto-detected |
|
| macOS | VideoToolbox | Auto-detected |
|
||||||
| Linux | VAAPI | Auto-detected |
|
| Linux | VAAPI | Auto-detected |
|
||||||
|
| Intel | QSV | Auto-detected |
|
||||||
|
|
||||||
GPU encoding is automatically used when available. To check your system's capabilities:
|
When GPU encoding is enabled, Hyperframes detects the available FFmpeg hardware encoder automatically. To check your system's capabilities:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx hyperframes doctor
|
npx hyperframes doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The CLI enables local Chrome/WebGL GPU capture automatically and supports `--no-browser-gpu` as an opt-out. When using the producer API directly, pass an engine config override:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resolveConfig } from '@hyperframes/producer';
|
||||||
|
|
||||||
|
const job = createRenderJob({
|
||||||
|
fps: 30,
|
||||||
|
quality: 'standard',
|
||||||
|
producerConfig: resolveConfig({ browserGpuMode: 'hardware' }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Additional Exports
|
## Additional Exports
|
||||||
|
|
||||||
The producer also re-exports key engine functionality for convenience:
|
The producer also re-exports key engine functionality for convenience:
|
||||||
|
|||||||
+1
-1
@@ -237,6 +237,6 @@ The agent handles scaffolding, animation, and rendering. See the [prompting guid
|
|||||||
Start from built-in examples like Warm Grain and Swiss Grid
|
Start from built-in examples like Warm Grain and Swiss Grid
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Rendering" icon="film" href="/guides/rendering">
|
<Card title="Rendering" icon="film" href="/guides/rendering">
|
||||||
Explore render options: quality presets, Docker mode, and GPU encoding
|
Explore render options: quality presets, Docker mode, and GPU acceleration
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const producerState = vi.hoisted(() => ({
|
||||||
|
createdJobs: [] as Array<Record<string, unknown>>,
|
||||||
|
resolveConfigCalls: [] as Array<Record<string, unknown>>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../utils/producer.js", () => ({
|
||||||
|
loadProducer: vi.fn(async () => ({
|
||||||
|
resolveConfig: vi.fn((overrides: Record<string, unknown>) => {
|
||||||
|
producerState.resolveConfigCalls.push(overrides);
|
||||||
|
return { ...overrides, resolved: true };
|
||||||
|
}),
|
||||||
|
createRenderJob: vi.fn((config: Record<string, unknown>) => {
|
||||||
|
producerState.createdJobs.push(config);
|
||||||
|
return { config, progress: 100 };
|
||||||
|
}),
|
||||||
|
executeRenderJob: vi.fn(async () => undefined),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../telemetry/events.js", () => ({
|
||||||
|
trackRenderComplete: vi.fn(),
|
||||||
|
trackRenderError: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("renderLocal browser GPU config", () => {
|
||||||
|
const savedEnv = new Map<string, string | undefined>();
|
||||||
|
|
||||||
|
function setEnv(key: string, value: string) {
|
||||||
|
savedEnv.set(key, process.env[key]);
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
producerState.createdJobs = [];
|
||||||
|
producerState.resolveConfigCalls = [];
|
||||||
|
savedEnv.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const [key, value] of savedEnv) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes an explicit software override for --no-browser-gpu even when env requests hardware", async () => {
|
||||||
|
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||||||
|
|
||||||
|
const { renderLocal } = await import("./render.js");
|
||||||
|
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||||
|
fps: 30,
|
||||||
|
quality: "standard",
|
||||||
|
format: "mp4",
|
||||||
|
gpu: false,
|
||||||
|
browserGpu: false,
|
||||||
|
hdrMode: "auto",
|
||||||
|
quiet: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "software" });
|
||||||
|
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
|
||||||
|
browserGpuMode: "software",
|
||||||
|
resolved: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes an explicit hardware override for default local browser GPU", async () => {
|
||||||
|
const { renderLocal } = await import("./render.js");
|
||||||
|
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||||
|
fps: 30,
|
||||||
|
quality: "standard",
|
||||||
|
format: "mp4",
|
||||||
|
gpu: false,
|
||||||
|
browserGpu: true,
|
||||||
|
hdrMode: "auto",
|
||||||
|
quiet: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "hardware" });
|
||||||
|
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
|
||||||
|
browserGpuMode: "hardware",
|
||||||
|
resolved: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves browser GPU from CLI flags, Docker mode, and env fallback", async () => {
|
||||||
|
const { resolveBrowserGpuForCli } = await import("./render.js");
|
||||||
|
|
||||||
|
expect(resolveBrowserGpuForCli(false, undefined, undefined)).toBe(true);
|
||||||
|
expect(resolveBrowserGpuForCli(false, undefined, "hardware")).toBe(true);
|
||||||
|
expect(resolveBrowserGpuForCli(false, undefined, "software")).toBe(false);
|
||||||
|
expect(resolveBrowserGpuForCli(false, true, "software")).toBe(true);
|
||||||
|
expect(resolveBrowserGpuForCli(false, false, "hardware")).toBe(false);
|
||||||
|
expect(resolveBrowserGpuForCli(true, undefined, "hardware")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ export const examples: Example[] = [
|
|||||||
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
|
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
|
||||||
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
|
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
|
||||||
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
|
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
|
||||||
|
["Opt out of browser GPU render", "hyperframes render --no-browser-gpu --output cpu.mp4"],
|
||||||
["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"],
|
["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"],
|
||||||
];
|
];
|
||||||
import { cpus, freemem, tmpdir } from "node:os";
|
import { cpus, freemem, tmpdir } from "node:os";
|
||||||
@@ -99,6 +100,11 @@ export default defineCommand({
|
|||||||
description: "Target video bitrate such as 10M. Mutually exclusive with --crf.",
|
description: "Target video bitrate such as 10M. Mutually exclusive with --crf.",
|
||||||
},
|
},
|
||||||
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
|
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
|
||||||
|
"browser-gpu": {
|
||||||
|
type: "boolean",
|
||||||
|
description:
|
||||||
|
"Use host GPU acceleration for Chrome/WebGL capture. Enabled by default for local renders; use --no-browser-gpu to opt out.",
|
||||||
|
},
|
||||||
quiet: {
|
quiet: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
description: "Suppress verbose output",
|
description: "Suppress verbose output",
|
||||||
@@ -186,6 +192,8 @@ export default defineCommand({
|
|||||||
|
|
||||||
const useDocker = args.docker ?? false;
|
const useDocker = args.docker ?? false;
|
||||||
const useGpu = args.gpu ?? false;
|
const useGpu = args.gpu ?? false;
|
||||||
|
const browserGpuArg = args["browser-gpu"];
|
||||||
|
const useBrowserGpu = resolveBrowserGpuForCli(useDocker, browserGpuArg);
|
||||||
const quiet = args.quiet ?? false;
|
const quiet = args.quiet ?? false;
|
||||||
const strictAll = args["strict-all"] ?? false;
|
const strictAll = args["strict-all"] ?? false;
|
||||||
const strictErrors = (args.strict ?? false) || strictAll;
|
const strictErrors = (args.strict ?? false) || strictAll;
|
||||||
@@ -197,6 +205,15 @@ export default defineCommand({
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useDocker && browserGpuArg === true) {
|
||||||
|
errorBox(
|
||||||
|
"Browser GPU is local-only",
|
||||||
|
"--browser-gpu uses the host Chrome GPU backend. Docker mode keeps browser rendering deterministic and does not expose a cross-platform Chrome GPU backend.",
|
||||||
|
"Run without --docker, or use --gpu for Docker GPU encoding where your Docker host supports GPU passthrough.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
let crf: number | undefined;
|
let crf: number | undefined;
|
||||||
if (crfRaw != null) {
|
if (crfRaw != null) {
|
||||||
const parsed = Number(crfRaw);
|
const parsed = Number(crfRaw);
|
||||||
@@ -227,6 +244,13 @@ export default defineCommand({
|
|||||||
c.dim(" \u2192 " + outputPath),
|
c.dim(" \u2192 " + outputPath),
|
||||||
);
|
);
|
||||||
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
|
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
|
||||||
|
if (useGpu || useBrowserGpu) {
|
||||||
|
const gpuModes = [
|
||||||
|
useGpu ? "encoder GPU" : null,
|
||||||
|
useBrowserGpu ? "browser GPU (auto)" : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
console.log(c.dim(" GPU: " + gpuModes.join(" + ")));
|
||||||
|
}
|
||||||
console.log("");
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +336,7 @@ export default defineCommand({
|
|||||||
format,
|
format,
|
||||||
workers,
|
workers,
|
||||||
gpu: useGpu,
|
gpu: useGpu,
|
||||||
|
browserGpu: useBrowserGpu,
|
||||||
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
||||||
crf,
|
crf,
|
||||||
videoBitrate,
|
videoBitrate,
|
||||||
@@ -324,6 +349,7 @@ export default defineCommand({
|
|||||||
format,
|
format,
|
||||||
workers,
|
workers,
|
||||||
gpu: useGpu,
|
gpu: useGpu,
|
||||||
|
browserGpu: useBrowserGpu,
|
||||||
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
||||||
crf,
|
crf,
|
||||||
videoBitrate,
|
videoBitrate,
|
||||||
@@ -340,6 +366,7 @@ interface RenderOptions {
|
|||||||
format: "mp4" | "webm" | "mov";
|
format: "mp4" | "webm" | "mov";
|
||||||
workers?: number;
|
workers?: number;
|
||||||
gpu: boolean;
|
gpu: boolean;
|
||||||
|
browserGpu: boolean;
|
||||||
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
||||||
crf?: number;
|
crf?: number;
|
||||||
videoBitrate?: string;
|
videoBitrate?: string;
|
||||||
@@ -347,6 +374,17 @@ interface RenderOptions {
|
|||||||
browserPath?: string;
|
browserPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveBrowserGpuForCli(
|
||||||
|
useDocker: boolean,
|
||||||
|
browserGpuArg: boolean | undefined,
|
||||||
|
envMode = process.env.PRODUCER_BROWSER_GPU_MODE,
|
||||||
|
): boolean {
|
||||||
|
if (useDocker) return false;
|
||||||
|
if (browserGpuArg !== undefined) return browserGpuArg;
|
||||||
|
if (envMode === "software") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";
|
const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";
|
||||||
|
|
||||||
function dockerImageTag(version: string): string {
|
function dockerImageTag(version: string): string {
|
||||||
@@ -464,6 +502,7 @@ async function renderDocker(
|
|||||||
format: options.format,
|
format: options.format,
|
||||||
workers: options.workers,
|
workers: options.workers,
|
||||||
gpu: options.gpu,
|
gpu: options.gpu,
|
||||||
|
browserGpu: options.browserGpu,
|
||||||
hdrMode: options.hdrMode,
|
hdrMode: options.hdrMode,
|
||||||
crf: options.crf,
|
crf: options.crf,
|
||||||
videoBitrate: options.videoBitrate,
|
videoBitrate: options.videoBitrate,
|
||||||
@@ -508,7 +547,7 @@ async function renderDocker(
|
|||||||
printRenderComplete(outputPath, elapsed, options.quiet);
|
printRenderComplete(outputPath, elapsed, options.quiet);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderLocal(
|
export async function renderLocal(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
options: RenderOptions,
|
options: RenderOptions,
|
||||||
@@ -530,6 +569,9 @@ async function renderLocal(
|
|||||||
format: options.format,
|
format: options.format,
|
||||||
workers: options.workers,
|
workers: options.workers,
|
||||||
useGpu: options.gpu,
|
useGpu: options.gpu,
|
||||||
|
producerConfig: producer.resolveConfig({
|
||||||
|
browserGpuMode: options.browserGpu ? "hardware" : "software",
|
||||||
|
}),
|
||||||
hdrMode: options.hdrMode,
|
hdrMode: options.hdrMode,
|
||||||
crf: options.crf,
|
crf: options.crf,
|
||||||
videoBitrate: options.videoBitrate,
|
videoBitrate: options.videoBitrate,
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ Requires: Docker installed and running.
|
|||||||
- `-w, --workers` — Parallel workers 1-8 (default: auto)
|
- `-w, --workers` — Parallel workers 1-8 (default: auto)
|
||||||
- `--crf` — Override encoder CRF (mutually exclusive with `--video-bitrate`)
|
- `--crf` — Override encoder CRF (mutually exclusive with `--video-bitrate`)
|
||||||
- `--video-bitrate` — Target video bitrate such as `10M` (mutually exclusive with `--crf`)
|
- `--video-bitrate` — Target video bitrate such as `10M` (mutually exclusive with `--crf`)
|
||||||
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, VAAPI)
|
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, VAAPI, QSV)
|
||||||
|
- `--browser-gpu` / `--no-browser-gpu` — Use or opt out of host GPU acceleration for local Chrome/WebGL capture (enabled by default for local renders, disabled in Docker)
|
||||||
- `-o, --output` — Custom output path
|
- `-o, --output` — Custom output path
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- Use `draft` quality for fast previews during development
|
- Use `draft` quality for fast previews during development
|
||||||
|
- Local renders use browser GPU capture automatically; use `--no-browser-gpu` to compare against the software-browser path
|
||||||
|
- Use `--gpu` when a local render also benefits from hardware FFmpeg encoding
|
||||||
- Use `npx hyperframes benchmark` to find optimal settings
|
- Use `npx hyperframes benchmark` to find optimal settings
|
||||||
- 4 workers is usually the sweet spot for most compositions
|
- 4 workers is usually the sweet spot for most compositions
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const BASE: DockerRenderOptions = {
|
|||||||
quality: "standard",
|
quality: "standard",
|
||||||
format: "mp4",
|
format: "mp4",
|
||||||
gpu: false,
|
gpu: false,
|
||||||
|
browserGpu: false,
|
||||||
hdrMode: "auto",
|
hdrMode: "auto",
|
||||||
crf: undefined,
|
crf: undefined,
|
||||||
videoBitrate: undefined,
|
videoBitrate: undefined,
|
||||||
@@ -42,6 +43,7 @@ describe("buildDockerRunArgs", () => {
|
|||||||
"standard",
|
"standard",
|
||||||
"--format",
|
"--format",
|
||||||
"mp4",
|
"mp4",
|
||||||
|
"--no-browser-gpu",
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -91,6 +93,7 @@ describe("buildDockerRunArgs", () => {
|
|||||||
"18",
|
"18",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"--gpu",
|
"--gpu",
|
||||||
|
"--no-browser-gpu",
|
||||||
"--hdr",
|
"--hdr",
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -139,6 +142,11 @@ describe("buildDockerRunArgs", () => {
|
|||||||
expect(on).toContain("--gpu");
|
expect(on).toContain("--gpu");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forces software browser capture inside Docker", () => {
|
||||||
|
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||||
|
expect(args).toContain("--no-browser-gpu");
|
||||||
|
});
|
||||||
|
|
||||||
it("forwards every renderer-shaped option (regression tripwire for silent drops)", () => {
|
it("forwards every renderer-shaped option (regression tripwire for silent drops)", () => {
|
||||||
const args = buildDockerRunArgs({
|
const args = buildDockerRunArgs({
|
||||||
...FIXED_INPUT,
|
...FIXED_INPUT,
|
||||||
@@ -148,6 +156,7 @@ describe("buildDockerRunArgs", () => {
|
|||||||
format: "webm",
|
format: "webm",
|
||||||
workers: 8,
|
workers: 8,
|
||||||
gpu: true,
|
gpu: true,
|
||||||
|
browserGpu: false,
|
||||||
hdrMode: "force-hdr",
|
hdrMode: "force-hdr",
|
||||||
crf: 16,
|
crf: 16,
|
||||||
videoBitrate: undefined,
|
videoBitrate: undefined,
|
||||||
@@ -165,6 +174,7 @@ describe("buildDockerRunArgs", () => {
|
|||||||
expect(args).toContain("16");
|
expect(args).toContain("16");
|
||||||
expect(args).toContain("--quiet");
|
expect(args).toContain("--quiet");
|
||||||
expect(args).toContain("--gpu");
|
expect(args).toContain("--gpu");
|
||||||
|
expect(args).toContain("--no-browser-gpu");
|
||||||
expect(args).toContain("--hdr");
|
expect(args).toContain("--hdr");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface DockerRenderOptions {
|
|||||||
format: "mp4" | "webm" | "mov";
|
format: "mp4" | "webm" | "mov";
|
||||||
workers?: number;
|
workers?: number;
|
||||||
gpu: boolean;
|
gpu: boolean;
|
||||||
|
browserGpu: boolean;
|
||||||
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
||||||
crf?: number;
|
crf?: number;
|
||||||
videoBitrate?: string;
|
videoBitrate?: string;
|
||||||
@@ -59,6 +60,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
|||||||
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
|
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
|
||||||
...(options.quiet ? ["--quiet"] : []),
|
...(options.quiet ? ["--quiet"] : []),
|
||||||
...(options.gpu ? ["--gpu"] : []),
|
...(options.gpu ? ["--gpu"] : []),
|
||||||
|
...(options.browserGpu ? [] : ["--no-browser-gpu"]),
|
||||||
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),
|
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),
|
||||||
...(options.hdrMode === "force-sdr" ? ["--sdr"] : []),
|
...(options.hdrMode === "force-sdr" ? ["--sdr"] : []),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe("resolveConfig", () => {
|
|||||||
expect(config.quality).toBe("standard");
|
expect(config.quality).toBe("standard");
|
||||||
expect(config.format).toBe("jpeg");
|
expect(config.format).toBe("jpeg");
|
||||||
expect(config.jpegQuality).toBe(80);
|
expect(config.jpegQuality).toBe(80);
|
||||||
|
expect(config.browserGpuMode).toBe("software");
|
||||||
expect(config.audioGain).toBe(1);
|
expect(config.audioGain).toBe(1);
|
||||||
expect(config.debug).toBe(false);
|
expect(config.debug).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -66,6 +67,20 @@ describe("resolveConfig", () => {
|
|||||||
expect(config.disableGpu).toBe(false);
|
expect(config.disableGpu).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads browser GPU mode from env", () => {
|
||||||
|
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
|
||||||
|
|
||||||
|
const config = resolveConfig();
|
||||||
|
expect(config.browserGpuMode).toBe("hardware");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to software browser GPU mode for invalid env values", () => {
|
||||||
|
setEnv("PRODUCER_BROWSER_GPU_MODE", "native");
|
||||||
|
|
||||||
|
const config = resolveConfig();
|
||||||
|
expect(config.browserGpuMode).toBe("software");
|
||||||
|
});
|
||||||
|
|
||||||
it("explicit overrides take precedence over env vars", () => {
|
it("explicit overrides take precedence over env vars", () => {
|
||||||
setEnv("PRODUCER_CORES_PER_WORKER", "5");
|
setEnv("PRODUCER_CORES_PER_WORKER", "5");
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export interface EngineConfig {
|
|||||||
// ── Browser ──────────────────────────────────────────────────────────
|
// ── Browser ──────────────────────────────────────────────────────────
|
||||||
chromePath?: string;
|
chromePath?: string;
|
||||||
disableGpu: boolean;
|
disableGpu: boolean;
|
||||||
|
/**
|
||||||
|
* Chrome/WebGL rendering backend. "software" keeps the existing SwiftShader
|
||||||
|
* path for reproducible output; "hardware" lets Chrome use the host GPU.
|
||||||
|
*/
|
||||||
|
browserGpuMode: "software" | "hardware";
|
||||||
enableBrowserPool: boolean;
|
enableBrowserPool: boolean;
|
||||||
browserTimeout: number;
|
browserTimeout: number;
|
||||||
protocolTimeout: number;
|
protocolTimeout: number;
|
||||||
@@ -113,6 +118,7 @@ export const DEFAULT_CONFIG: EngineConfig = {
|
|||||||
largeRenderThreshold: 1000,
|
largeRenderThreshold: 1000,
|
||||||
|
|
||||||
disableGpu: false,
|
disableGpu: false,
|
||||||
|
browserGpuMode: "software",
|
||||||
enableBrowserPool: false,
|
enableBrowserPool: false,
|
||||||
browserTimeout: 120_000,
|
browserTimeout: 120_000,
|
||||||
protocolTimeout: 300_000,
|
protocolTimeout: 300_000,
|
||||||
@@ -158,6 +164,11 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
|||||||
if (raw === undefined) return fallback;
|
if (raw === undefined) return fallback;
|
||||||
return raw === "true";
|
return raw === "true";
|
||||||
};
|
};
|
||||||
|
const envBrowserGpuMode = (): EngineConfig["browserGpuMode"] => {
|
||||||
|
const raw = env("PRODUCER_BROWSER_GPU_MODE");
|
||||||
|
if (raw === "hardware" || raw === "software") return raw;
|
||||||
|
return DEFAULT_CONFIG.browserGpuMode;
|
||||||
|
};
|
||||||
|
|
||||||
// Env-var layer (backward compat)
|
// Env-var layer (backward compat)
|
||||||
const fromEnv: Partial<EngineConfig> = {
|
const fromEnv: Partial<EngineConfig> = {
|
||||||
@@ -171,6 +182,7 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
|||||||
|
|
||||||
chromePath: env("PRODUCER_HEADLESS_SHELL_PATH"),
|
chromePath: env("PRODUCER_HEADLESS_SHELL_PATH"),
|
||||||
disableGpu: envBool("PRODUCER_DISABLE_GPU", DEFAULT_CONFIG.disableGpu),
|
disableGpu: envBool("PRODUCER_DISABLE_GPU", DEFAULT_CONFIG.disableGpu),
|
||||||
|
browserGpuMode: envBrowserGpuMode(),
|
||||||
enableBrowserPool: envBool("PRODUCER_ENABLE_BROWSER_POOL", DEFAULT_CONFIG.enableBrowserPool),
|
enableBrowserPool: envBool("PRODUCER_ENABLE_BROWSER_POOL", DEFAULT_CONFIG.enableBrowserPool),
|
||||||
browserTimeout: envNum("PRODUCER_PUPPETEER_LAUNCH_TIMEOUT_MS", DEFAULT_CONFIG.browserTimeout),
|
browserTimeout: envNum("PRODUCER_PUPPETEER_LAUNCH_TIMEOUT_MS", DEFAULT_CONFIG.browserTimeout),
|
||||||
protocolTimeout: envNum(
|
protocolTimeout: envNum(
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildChromeArgs } from "./browserManager.js";
|
||||||
|
|
||||||
|
describe("buildChromeArgs browser GPU mode", () => {
|
||||||
|
const base = { width: 1920, height: 1080 };
|
||||||
|
|
||||||
|
it("uses SwiftShader software GL by default for reproducible local renders", () => {
|
||||||
|
const args = buildChromeArgs(base);
|
||||||
|
expect(args).toContain("--use-gl=angle");
|
||||||
|
expect(args).toContain("--use-angle=swiftshader");
|
||||||
|
expect(args).not.toContain("--enable-gpu-rasterization");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => {
|
||||||
|
const args = buildChromeArgs({ ...base, platform: "darwin" }, { browserGpuMode: "hardware" });
|
||||||
|
expect(args).toContain("--use-gl=angle");
|
||||||
|
expect(args).toContain("--use-angle=metal");
|
||||||
|
expect(args).toContain("--enable-gpu-rasterization");
|
||||||
|
expect(args).not.toContain("--use-angle=swiftshader");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses D3D11-backed ANGLE for hardware browser GPU mode on Windows", () => {
|
||||||
|
const args = buildChromeArgs({ ...base, platform: "win32" }, { browserGpuMode: "hardware" });
|
||||||
|
expect(args).toContain("--use-gl=angle");
|
||||||
|
expect(args).toContain("--use-angle=d3d11");
|
||||||
|
expect(args).toContain("--enable-gpu-rasterization");
|
||||||
|
expect(args).not.toContain("--use-angle=swiftshader");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses EGL for hardware browser GPU mode on Linux", () => {
|
||||||
|
const args = buildChromeArgs({ ...base, platform: "linux" }, { browserGpuMode: "hardware" });
|
||||||
|
expect(args).toContain("--use-gl=egl");
|
||||||
|
expect(args).toContain("--enable-gpu-rasterization");
|
||||||
|
expect(args).not.toContain("--use-gl=angle");
|
||||||
|
expect(args).not.toContain("--use-angle=swiftshader");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps --disable-gpu authoritative when requested", () => {
|
||||||
|
const args = buildChromeArgs(
|
||||||
|
{ ...base, platform: "darwin" },
|
||||||
|
{ browserGpuMode: "hardware", disableGpu: true },
|
||||||
|
);
|
||||||
|
expect(args).toContain("--disable-gpu");
|
||||||
|
expect(args).toContain("--use-angle=swiftshader");
|
||||||
|
expect(args).not.toContain("--use-angle=metal");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -238,12 +238,18 @@ export interface BuildChromeArgsOptions {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
captureMode?: CaptureMode;
|
captureMode?: CaptureMode;
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildChromeArgs(
|
export function buildChromeArgs(
|
||||||
options: BuildChromeArgsOptions,
|
options: BuildChromeArgsOptions,
|
||||||
config?: Partial<Pick<EngineConfig, "disableGpu" | "chromePath">>,
|
config?: Partial<Pick<EngineConfig, "browserGpuMode" | "disableGpu" | "chromePath">>,
|
||||||
): string[] {
|
): string[] {
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
|
const gpuDisabled = config?.disableGpu ?? DEFAULT_CONFIG.disableGpu;
|
||||||
|
const browserGpuMode = gpuDisabled
|
||||||
|
? "software"
|
||||||
|
: (config?.browserGpuMode ?? DEFAULT_CONFIG.browserGpuMode);
|
||||||
// Chrome flags tuned for headless rendering performance. The set below is a
|
// Chrome flags tuned for headless rendering performance. The set below is a
|
||||||
// fairly standard "headless-for-capture" configuration — similar profiles
|
// fairly standard "headless-for-capture" configuration — similar profiles
|
||||||
// appear in Puppeteer's defaults, Playwright, Remotion, and Chrome's own
|
// appear in Puppeteer's defaults, Playwright, Remotion, and Chrome's own
|
||||||
@@ -254,8 +260,7 @@ export function buildChromeArgs(
|
|||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
"--enable-webgl",
|
"--enable-webgl",
|
||||||
"--ignore-gpu-blocklist",
|
"--ignore-gpu-blocklist",
|
||||||
"--use-gl=angle",
|
...getBrowserGpuArgs(browserGpuMode, platform),
|
||||||
"--use-angle=swiftshader",
|
|
||||||
"--font-render-hinting=none",
|
"--font-render-hinting=none",
|
||||||
"--force-color-profile=srgb",
|
"--force-color-profile=srgb",
|
||||||
`--window-size=${options.width},${options.height}`,
|
`--window-size=${options.width},${options.height}`,
|
||||||
@@ -301,9 +306,28 @@ export function buildChromeArgs(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gpuDisabled = config?.disableGpu ?? DEFAULT_CONFIG.disableGpu;
|
|
||||||
if (gpuDisabled) {
|
if (gpuDisabled) {
|
||||||
chromeArgs.push("--disable-gpu");
|
chromeArgs.push("--disable-gpu");
|
||||||
}
|
}
|
||||||
return chromeArgs;
|
return chromeArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrowserGpuArgs(
|
||||||
|
mode: EngineConfig["browserGpuMode"],
|
||||||
|
platform: NodeJS.Platform,
|
||||||
|
): string[] {
|
||||||
|
if (mode === "software") {
|
||||||
|
return ["--use-gl=angle", "--use-angle=swiftshader"];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case "darwin":
|
||||||
|
return ["--use-gl=angle", "--use-angle=metal", "--enable-gpu-rasterization"];
|
||||||
|
case "win32":
|
||||||
|
return ["--use-gl=angle", "--use-angle=d3d11", "--enable-gpu-rasterization"];
|
||||||
|
case "linux":
|
||||||
|
return ["--use-gl=egl", "--enable-gpu-rasterization"];
|
||||||
|
default:
|
||||||
|
return ["--enable-gpu-rasterization"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ function createConfig(): EngineConfig {
|
|||||||
minParallelFrames: 120,
|
minParallelFrames: 120,
|
||||||
largeRenderThreshold: 1000,
|
largeRenderThreshold: 1000,
|
||||||
disableGpu: false,
|
disableGpu: false,
|
||||||
|
browserGpuMode: "software",
|
||||||
enableBrowserPool: false,
|
enableBrowserPool: false,
|
||||||
browserTimeout: 120000,
|
browserTimeout: 120000,
|
||||||
protocolTimeout: 300000,
|
protocolTimeout: 300000,
|
||||||
|
|||||||
Reference in New Issue
Block a user