Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e412b67
feat(html-local): add shared-texture GPU video export
rponeawa Oct 24, 2026
a78652c
perf(html-local): decode source video directly in GPU scenes
rponeawa Oct 24, 2026
61eb15c
perf(html-local): pipeline GPU snapshots with bounded ordered encoding
rponeawa Oct 24, 2026
4e77b8f
fix(html-local): wait for descendant termination during exit cleanup
rponeawa Oct 24, 2026
ae2e0b3
fix(html-local): manage pinned Electron and package browser media assets
rponeawa Oct 24, 2026
29bde0f
fix(html-local): release GPU surfaces before mux and output validation
rponeawa Oct 24, 2026
a6a9171
fix(html-local): launch macOS renderer as a background app
rponeawa Oct 24, 2026
3b360f2
fix(html-local): isolate video layer decoding and serialize texture i…
rponeawa Oct 24, 2026
608b595
perf(html-local): tune default source and frame queue budgets
rponeawa Oct 24, 2026
97ff3b4
chore(html-local): add automated GPU benchmark and partial results
rponeawa Oct 24, 2026
69448af
docs(html-local): use recorded MG export duration
rponeawa Oct 24, 2026
feefcbb
test(html-local): align budget expectations with configured defaults
rponeawa Oct 26, 2026
ca0e9cb
fix(html-local): separate benchmark fixtures and complete worker comp…
rponeawa Oct 26, 2026
39753c2
chore(html-local): complete GPU benchmark results and source-based re…
rponeawa Oct 26, 2026
2ed24db
ci: use the Ubuntu archive for minimal FFmpeg installation
rponeawa Oct 26, 2026
c080acd
fix(html-local): preserve GPU export frame rates across FFmpeg versions
rponeawa Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ jobs:
- name: Install ffmpeg on Linux
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
if [ -f "$source" ]; then
sudo sed -i -E 's|https?://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source"
fi
done
sudo apt-get -o Acquire::Retries=3 update
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends ffmpeg

- name: Install ffmpeg on Windows
if: runner.os == 'Windows'
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ jobs:
- name: Prepare the local media toolchain on Linux
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
if [ -f "$source" ]; then
sudo sed -i -E 's|https?://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source"
fi
done
sudo apt-get -o Acquire::Retries=3 update
sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends ffmpeg
- name: Prepare the local media toolchain on Windows
if: runner.os == 'Windows'
run: choco install ffmpeg --yes --no-progress
Expand Down
202 changes: 142 additions & 60 deletions packages/provider-html-local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,161 @@ Trusted local implementation of:
- `@hypit/html-program@1#rasterize-visual` → `TimelineVisual`;
- `@hypit/html-program@1#rasterize-frames` → ordered PNG BlobRefs.

The Provider consumes the typed `HtmlProgram` directly. It serves the staged
program to independent Puppeteer/Chrome processes, applies absolute integer
frames through `window.__hypitFrameProgram`, decodes compiler-declared source
video frames with FFmpeg, captures PNG pixels through Chrome DevTools Protocol,
and streams ordered frames into one H.264 encoder. It has no external HTML video
engine or producer dependency.
The Provider stages a typed `HtmlProgram`, applies absolute integer frames through
`window.__hypitFrameProgram`, and supplies compiler-declared video samples.
It supports two video export configurations:

Filesystem staging is private to this local Provider. `@hypit/html-program`
retains only the portable program and its pure materialization function; it
does not expose a Node project/staging subpath for hypothetical executors.
Inspection of staged Compositable Surface files and probing the selected media
tools are private here for the same reason: they validate inputs used by this
executor and do not establish a shared media-execution ABI.
| Configuration | Source video | Capture and encoding |
| --- | --- | --- |
| `chromePath`, or managed Chrome selection | Bounded FFmpeg PNG decode store | Puppeteer PNG capture and ordered FFmpeg H.264 encoding |
| `renderer: "electron"`, or `electronPath` | Mediabunny demuxing, WebCodecs decoding, Canvas video layers | Electron shared GPU textures, Canvas cropping, WebCodecs H.264 encoding, FFmpeg MP4 muxing |

## Runtime boundary
PNG frame requests use the selected Chrome browser. Filesystem staging, source
probing and decoded-frame leases belong to this Provider.

The Endpoint facet is `rasterize`. Runtime configuration owns deployment policy:
## Runtime configuration

- `workers`: fixed browser count or `auto`;
The Endpoint facet is `rasterize`. Runtime configuration owns execution policy:

- `workers`: fixed worker count or `auto`;
- `maxWorkers`: automatic ceiling;
- `defaultConcurrency`: whole requests admitted in this Endpoint pool;
- `browserCapacity`: Chrome slots reserved across admitted requests;
- `browserCapacity`: worker slots reserved across admitted requests;
- `chromePath`, or exact `browserVersion` plus `browserCacheDirectory`;
- `renderer`: `chromium` (default) or `electron` for GPU video export;
- `electronPath`: a user-managed Electron executable, selecting the Electron renderer;
- `electronCacheDirectory`: managed Electron installation and archive cache directory;
- `electronDownloadBaseUrl`: optional Electron release archive base URL;
- `ffmpegPath` and `ffprobePath`;
- optional process/stage deadlines and byte budgets.

The package recommendation is stored as `hypit.htmlBrowser.version`. Managed
Chrome defaults to `~/.cache/hypit/html-rasterizer/chrome`; its ManagedProgram
state is stored below `.hypit-html-browser-program`. Browser installation occurs
only through explicit Runtime preparation:
For GPU export with declared video sources, `auto` uses one scene worker, sharing
its source decoders and cache across the timeline. Pure motion graphics use
parallel scenes up to the admitted worker ceiling. An explicit worker count
enables that many scenes, limited by the requested frame span. PNG capture adjusts its
active workers using measured throughput.
GPU export requires `browserGpu: "auto"` or `"hardware"`, even canvas dimensions,
shared-texture support, browser support for each source codec and a supported
WebCodecs H.264 configuration. Unsupported configurations fail with a diagnostic.

The Chrome recommendation is stored as `hypit.htmlBrowser.version`. Managed Chrome
defaults to `~/.cache/hypit/html-rasterizer/chrome`; its ManagedProgram state is
stored below `.hypit-html-browser-program`. Browser installation occurs through
explicit Runtime preparation:

```sh
hypit programs prepare --runtime ./hypit.runtime.json --endpoint html.local
```

Inspection and execution never search environment variables or silently choose a
different system browser. `chromePath` selects a user-managed executable;
`browserVersion` selects one exact managed Chrome for Testing version. The two
selection modes are mutually exclusive.

## Frame selection and parallelism

A continuous request retains original absolute frame identities. A sparse request
accepts strictly increasing, non-contiguous frames. Workers are independent
browser processes and may start at any requested frame; they do not wait for an
earlier worker to traverse the program.

The compiler's typed `frameSources` plan determines exact source-video samples,
including reverse, hold, loop and rational-rate mappings. The Provider probes
needed sources, decodes bounded windows lazily, shares decoded frames only within
the current execution, and leases them under `maxDecodedSourceBytes`. Chrome does
not independently decode those same videos. Arbitrary author HTML is not parsed
to infer source-time semantics.

Completed PNGs enter a bounded ordered sink. FFmpeg is the necessary local
serialization point for one MP4 stream, not a scheduler or shared metadata
service. Capture of later frames can continue while earlier frames are encoded.
Sparse frame output writes only requested PNGs and is bounded by
`maxRenderedBytes`.

## Lifecycle and failure

Each request owns its temporary directory, localhost server, Chrome processes,
decoded-frame store and encoder. Nothing is shared across requests. Cancellation
or failure closes page sessions, source leases, processes and resource I/O.
Process-tree cleanup is best effort after abnormal exits; hard OS termination
requires containment supplied by the deployment.
`chromePath` selects a user-managed Chrome executable; `browserVersion` selects
one exact managed Chrome for Testing version. These selections are mutually
exclusive. `renderer: "electron"` selects the Provider's pinned Electron release. Runtime
preparation installs both managed Chrome and Electron as needed; probes inspect
the selected installations. Electron archives are verified against the release
checksums shipped in the Provider manifest and extracted into a staging directory
before installation. Repeated preparation reuses healthy installations.
On macOS, preparation sets `LSUIElement=true` and applies and verifies a local
ad-hoc signature so the renderer starts as a background application.

`electronPath` selects a user-managed executable with the same pinned version and
is mutually exclusive with managed Electron cache and download configuration.
On macOS this executable must belong to a background app with `LSUIElement=true`.
Relative configured paths resolve against the Runtime data root. The default
Electron cache is `~/.cache/hypit/html-rasterizer/electron`.

Example `html.local` Endpoint configuration:

```json
{
"renderer": "electron",
"workers": "auto",
"quality": "standard"
}
```

The executor waits for page frame listeners, declared fonts, dynamic image
decodes and compositor settlement before accepting a screenshot. Readiness is
scoped to compiler-published capture roots when available; real global stylesheet
edits intentionally fall back to whole-document inspection.
The current pinned Electron release is 45.0.0. Custom download bases use
`<base>/v45.0.0/electron-v45.0.0-<platform>-<arch>.zip` and must serve the official
archive bytes. A failed download leaves the selected installation unavailable
until preparation succeeds.

On macOS the executable is inside `Electron.app/Contents/MacOS/Electron`; on
Windows it is `electron.exe`; on Linux it is `electron`. Electron's shared-texture
API is experimental. The integration has been exercised on macOS / Apple Silicon;
Windows and Linux require validation on their target GPU and driver combinations.
Linux additionally needs the display environment required by Electron.

## Frames and encoding

A continuous request retains original absolute frame identities. A sparse PNG
request accepts strictly increasing, non-contiguous frames. Workers can start at
any requested frame. The compiler's `frameSources` plan determines reverse, hold,
loop and rational-rate video sampling. PNG capture leases decoded images under
`maxDecodedSourceBytes` and places them over compiler-owned video elements.

GPU capture decodes source video inside each scene renderer. Chromium selects
hardware or software decoding according to source support. Forward sampling
keeps an independent decoder cursor for each video layer. Layers share the source
input and sample cache, so aligned requests reuse the same decoded frame. Reverse
sampling divides the cache budget across active layers and decodes bounded forward
intervals. Holds and loops reuse cached samples. Canvas layers retain the video
elements' styles and placement. Inactive layers release their decoder cursors.
`maxDecodedSourceBytes` defaults to 2.25 GiB, is divided across workers and bounds
retained sample bytes, allowing one oversized sample per worker. Decoder queues,
demuxer caches and compositor surfaces require additional memory. Worker count also multiplies
source decoding; more workers can reduce throughput for video-heavy projects.

Source color tags are preserved. Untagged SDR uses a BT.601 matrix and sRGB
transfer. Planar alpha samples retain source color metadata through an in-memory
copy before Canvas composition. The published Provider includes the Mediabunny
1.62.1 browser bundle, original source and license under `browser/mediabunny/`.
Mediabunny is distributed under
[MPL-2.0](https://github.com/Vanilagy/mediabunny/blob/main/LICENSE).

The GPU path renders each Program in a fixed-size same-origin iframe. A separate
32-bit frame identifier below the authored viewport identifies the accepted
texture. Native texture imports are submitted one at a time in ready-frame order;
snapshot copying and encoding proceed concurrently. Only the 8 KiB identifier
region is read back to the CPU. Canvas crops
that region before creating an immutable `VideoFrame` snapshot. Scenes can
prepare subsequent frames while a shared queue feeds snapshots to the encoder
in timeline order. Admission reserves space for the next required frame to
prevent later frames from blocking it.

`maxPendingFrameBytes` defaults to 128 MiB and controls admission using queued
snapshot pixels and pending compressed output. One oversized snapshot can enter
an empty queue. Encoding already in progress can produce compressed chunks above that admission threshold;
native encoder buffers and compositor surfaces require additional memory. Each
worker releases source decoders and samples when its capture completes. Scene
surfaces remain alive through the encoder flush.

WebCodecs uses H.264 High with `quality` latency and a hardware preference.
At 1920 × 1080 / 30 fps, `quality` selects target bitrates of 6 Mbps (`draft`),
12 Mbps (`standard`), or 24 Mbps (`high`). Targets scale with pixel count and frame
rate, with a 64 Kbps minimum. Actual hardware encoder availability is determined
by Chromium. FFmpeg assigns the requested rational frame time base and muxes the
encoded stream. PNG video export uses x264 CRF 28, 23, or 18 respectively.
These settings do not imply identical rate control or picture quality.

For PNG video capture, `maxPendingFrameBytes` bounds the ordered PNG queue.
`maxRenderedBytes` limits the final MP4 or total requested PNG bytes; GPU capture
also checks compressed bytes as they arrive. Output validation decodes every frame
and verifies dimensions, codec, exact frame rate and frame count before storage.

## Lifecycle and page readiness

Each request owns its temporary directory, localhost server, browser processes,
decoded-frame store and encoder. Cancellation and failure stop that execution and
release its resources. Process-tree cleanup is best effort after abnormal exits;
hard OS termination requires containment supplied by the deployment.

Frame preparation waits for page frame listeners, declared fonts and dynamic
image decodes. GPU capture updates its texture identifier after preparation and
accepts a shared surface only when its identifier matches. The authored page and
identifier participate in the same local frame-tree rendering update.
Authored asynchronous drawing must finish
inside the Program's frame completion hooks. Child pages rendered in another
renderer process are rejected by GPU capture; the iframe container makes
`window.top` refer to the capture shell.

## Direct use

Expand All @@ -85,10 +168,9 @@ import { rasterizeHtmlProgram } from "@hypit/provider-html-local";

const visual = await rasterizeHtmlProgram(
{ program, range: { startFrame: 240, endFrameExclusive: 360 } },
{ resources, workers: 4 },
{ resources, renderer: "electron", workers: "auto" },
);
```

Omit `range` for the complete program. Audio is not part of this result;
`@hypit/media-local` and the author assembly prepare audio and mux the
final artifact.
Omit `range` for the complete program. Audio is prepared and muxed by
`@hypit/media-local` and the author assembly.
32 changes: 30 additions & 2 deletions packages/provider-html-local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,43 @@
},
"files": [
"dist",
"README.md"
"src/gpu-worker.cjs",
"src/gpu-encoder.cjs",
"src/browser-video.mjs",
"README.md",
"browser"
],
"hypit": {
"activation": "./src/activation.ts",
"htmlBrowser": {
"version": "152.0.7928.2"
},
"htmlElectron": {
"version": "45.0.0",
"checksums": {
"electron-v45.0.0-darwin-arm64.zip": "896c3a50b148e89a158dca08a6996a022ddf39b23aac871e107f5b4087f8caa9",
"electron-v45.0.0-darwin-x64.zip": "7877c282e74a52f8d87949536a25735cf9982de1265b544c9cd258dd14eff4de",
"electron-v45.0.0-linux-arm64.zip": "a80d92b1ad6c111545a12632b8a75cedce614a41866e500810d4050d947f90e9",
"electron-v45.0.0-linux-x64.zip": "22f6a10bd3f7fef987ddbb379d3375ce664ad0c98a9a50f80c89f1718a0c3e3a",
"electron-v45.0.0-win32-arm64.zip": "fbae273960b24b81ab7a5cdcf34dd83a8781ee6799ccc621c1d85d5ce197252d",
"electron-v45.0.0-win32-x64.zip": "ae53887a23bb640369c93dbd38c408a2af5c0deb86f2fa76c6c244cb444b7f47"
}
},
"browserAssets": {
"mediabunny": [
"dist/bundles/mediabunny.min.mjs",
"LICENSE",
"src"
]
}
},
"dependencies": {
"@electron/get": "5.1.0",
"@hypit/html-program": "workspace:^0.1.0",
"@hypit/media": "workspace:^0.1.0",
"@puppeteer/browsers": "3.2.2",
"extract-zip": "2.0.1",
"proper-lockfile": "4.1.2",
"puppeteer-core": "25.10.0",
"tsx": "4.21.0"
},
Expand All @@ -32,8 +57,8 @@
"@fontsource-variable/inter": "5.3.0",
"@hypit/caption-fine": "workspace:*",
"@hypit/composition": "workspace:*",
"@hypit/executor": "workspace:*",
"@hypit/endpoint": "workspace:*",
"@hypit/executor": "workspace:*",
"@hypit/html-program": "workspace:*",
"@hypit/hypit": "workspace:*",
"@hypit/media": "workspace:*",
Expand All @@ -43,6 +68,9 @@
"@hypit/script": "workspace:*",
"@hypit/timeline": "workspace:*",
"@hypit/visual-track": "workspace:*",
"@types/proper-lockfile": "4.1.4",
"electron": "45.0.0",
"mediabunny": "1.62.1",
"sharp": "0.35.4"
}
}
Loading
Loading