-
Notifications
You must be signed in to change notification settings - Fork 0
feat(examples): add web-capture gateway #631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
streamer45
wants to merge
27
commits into
main
Choose a base branch
from
feat/web-capture-gateway
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
b0489fe
feat(examples): add web-capture gateway (clip/cast.streamkit.dev)
streamer45 0dedcd6
fix(web-capture): harden SSRF, session lifetime, and clip concurrency
streamer45 5ca0f1e
refactor(web-capture): embed pipeline YAML from template files
streamer45 170061f
fix(web-capture): close shutdown/create race, proxy + log correctness
streamer45 ce0e590
feat(web-capture): H.264/fMP4 cast for Safari/iOS; user-role token
streamer45 c8e1f75
refactor(web-capture): single host, output as the first path segment
streamer45 c8638e0
regression test
streamer45 008d3fb
fix(servo,web-capture): stop cross-session pixel leak; land clip firs…
streamkit-devin b2d9233
fix(servo): re-arm first-paint gate on viewport resize
streamkit-devin 1672946
feat(examples/web-capture): serve autoplay clip player page for brows…
streamkit-devin 6fdae5b
fix(plugins/servo): clear cached frame on URL change
streamkit-devin 7a8a517
fix(examples/web-capture): clamp max-concurrency to >=1, fix option e…
streamkit-devin 7d1f949
feat(transport::http::mse): add fMP4 support for H.264 casting
streamkit-devin 0837352
Merge origin/main into feat/web-capture-gateway
streamkit-devin 1cc9da4
fix(web-capture): close pipe reader if clip request build fails
streamkit-devin af7cef1
chore(plugins/servo): bump version to 0.1.1
streamkit-devin 6821c16
fix(web-capture): fail fast in proxyMSE when session reaped mid-retry
streamkit-devin d0288fc
fix(web-capture): block IPv4-compatible IPv6 and local-use NAT64 in S…
streamkit-devin debfd9a
perf(web-capture): defer target DNS lookup off the player-page path
streamkit-devin 8b748fb
refactor(servo): dedupe transparent-frame paths and surface make_curr…
streamkit-devin ff957fe
refactor(web-capture): dedupe gateway auth/maxViewers state and strip…
streamkit-devin d77d0a7
chore(deps): bump anyhow + wasmtime-wasi and ignore ttf-parser unmain…
streamkit-devin d4348e0
fix(web-capture): reject sessions whose create finishes after shutdown
streamkit-devin 4c782ff
fix(web-capture): derive MSE Content-Type fallback from the cast enco…
streamkit-devin 42fdbcc
fix(web-capture): clamp max-sessions/max-viewers and drop unused auth…
streamkit-devin 8b5ea1d
chore(servo): drop redundant uuid dev-dependency
streamkit-devin cf95e4a
chore(deps): ignore quick-xml RUSTSEC-2026-0194/0195 advisories via o…
streamkit-devin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /gateway |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| version: "2" | ||
| linters: | ||
| default: none | ||
| enable: | ||
| - bidichk | ||
| - errcheck | ||
| - govet | ||
| - ineffassign | ||
| - makezero | ||
| - misspell | ||
| - revive | ||
| - staticcheck | ||
| - unconvert | ||
| - unqueryvet | ||
| - unused | ||
| - whitespace | ||
| settings: | ||
| govet: | ||
| disable: | ||
| - fieldalignment | ||
| enable-all: true | ||
| unqueryvet: | ||
| check-sql-builders: true | ||
| issues: | ||
| max-issues-per-linter: 0 # no maximum | ||
| max-same-issues: 0 # no maximum | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # SPDX-FileCopyrightText: © 2025 StreamKit Contributors | ||
| # | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| FROM golang:1.24-bookworm AS build | ||
| WORKDIR /src | ||
| COPY . . | ||
| RUN CGO_ENABLED=0 go build -o /gateway ./cmd/gateway | ||
|
|
||
| FROM gcr.io/distroless/static-debian12 | ||
| COPY --from=build /gateway /gateway | ||
| EXPOSE 8080 | ||
| ENTRYPOINT ["/gateway"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # SPDX-FileCopyrightText: © 2025 StreamKit Contributors | ||
| # | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| .PHONY: build lint test run clean | ||
|
|
||
| build: | ||
| go build -o gateway ./cmd/gateway | ||
|
|
||
| lint: | ||
| golangci-lint run | ||
|
|
||
| test: | ||
| go test ./... | ||
|
|
||
| run: | ||
| go run ./cmd/gateway | ||
|
|
||
| clean: | ||
| rm -f gateway |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: © 2025 StreamKit Contributors | ||
|
|
||
| SPDX-License-Identifier: MPL-2.0 | ||
| --> | ||
|
|
||
| # Web Capture Gateway | ||
|
|
||
| Render any web page to video through a StreamKit backend. Paste a target URL straight onto the gateway host — no flags, no JSON: | ||
|
|
||
| - **clip** — a finite MP4 file (oneshot pipeline): `clip.streamkit.dev/example.com` | ||
| - **cast** — a live WebM stream (dynamic session): `cast.streamkit.dev/example.com` | ||
|
|
||
| The page is rendered by the [Servo](../../plugins/native/servo) browser engine on the backend. The gateway turns a pasteable URL into the multipart oneshot / dynamic-session calls the backend expects and, for `cast`, owns the session lifetime so abandoned streams are torn down instead of leaking a renderer. | ||
|
|
||
| ## URL shape | ||
|
|
||
| ``` | ||
| {host}/[options/]{target-url} | ||
| ``` | ||
|
|
||
| The target URL is taken **verbatim** as the path suffix (scheme optional, `https` assumed), so its own query string just rides along — no percent-encoding: | ||
|
|
||
| ``` | ||
| clip.streamkit.dev/grafana.example/d/abc?panel=3&from=now-6h | ||
| ``` | ||
|
|
||
| Options are an optional comma-separated `key=value` **first segment**: | ||
|
|
||
| ``` | ||
| clip.streamkit.dev/dur=30s/example.com # clip length (default 10s, capped at 60s) | ||
| cast.streamkit.dev/res=2560x1440/example.com # capture resolution (default 1920x1080) | ||
| clip.streamkit.dev/res=1280x720,dur=15s/example.com # combine, comma-separated | ||
| ``` | ||
|
|
||
| `dur` is `clip`-only; `res` (the capture resolution) applies to both modes. | ||
|
|
||
| Locally there are no subdomains, so select the mode with a path prefix instead: | ||
|
|
||
| ``` | ||
| http://127.0.0.1:8080/clip/dur=5s/example.com | ||
| http://127.0.0.1:8080/cast/example.com | ||
| ``` | ||
|
|
||
| The same URL serves a browser and a player/script: an address-bar visit (`Accept: text/html`) to a `cast` URL returns a tiny autoplay page; anything else (a `<video src>`, `curl`, `ffplay`) gets the raw stream/file. | ||
|
|
||
| ## Formats & browser support | ||
|
|
||
| | Mode | Format | Plays in | | ||
| |------|--------|----------| | ||
| | `clip` | MP4 / H.264, fragmented (plays inline as it renders) | Everywhere, including Safari/iOS | | ||
| | `cast` | WebM / VP9 (the only container the MSE transport emits) | Chromium & Firefox — **Safari/iOS may not play the live stream** | | ||
|
|
||
| Universal live playback would require the backend's `transport::http::mse` node to emit fragmented MP4; that's a possible follow-up, not part of v1. | ||
|
|
||
| ## Prereqs | ||
|
|
||
| - A StreamKit server with the **Servo plugin** built and loaded: | ||
| `just build-plugin-native-servo && just copy-plugins-native`. | ||
| - The gateway's token/role must be allowed to **create and destroy sessions** and to use the `plugin::native::servo` node. | ||
| - Go 1.24+. | ||
|
|
||
| ## Run | ||
|
|
||
| ```sh | ||
| cd examples/web-capture | ||
| go run ./cmd/gateway --listen :8080 --skit-url http://127.0.0.1:4545 | ||
|
|
||
| # clip → MP4 (plays inline in a browser; -o saves it) | ||
| curl -L 'http://127.0.0.1:8080/clip/dur=5s/example.com' -o clip.mp4 | ||
|
|
||
| # cast → open in a browser to watch live | ||
| xdg-open 'http://127.0.0.1:8080/cast/example.com' | ||
| ``` | ||
|
|
||
| ### Configuration | ||
|
|
||
| | Env | Flag | Default | Purpose | | ||
| |-----|------|---------|---------| | ||
| | `GATEWAY_LISTEN` | `--listen` | `:8080` | Listen address | | ||
| | `SKIT_URL` | `--skit-url` | `http://127.0.0.1:4545` | Backend URL | | ||
| | `SKIT_TOKEN` | `--token` | — | Bearer token sent to the backend | | ||
| | `GATEWAY_MAX_CONCURRENCY` | `--max-concurrency` | 4 | Max concurrent clip renders | | ||
| | `GATEWAY_CLIP_DEFAULT_SECS` | `--clip-default-secs` | 10 | Default clip duration | | ||
| | `GATEWAY_CLIP_MAX_SECS` | `--clip-max-secs` | 60 | Maximum clip duration | | ||
| | `GATEWAY_MAX_SESSIONS` | `--max-sessions` | 8 | Max concurrent live cast sessions | | ||
| | `GATEWAY_MAX_VIEWERS` | `--max-viewers` | 10 | Max viewers per cast stream | | ||
| | `GATEWAY_SESSION_IDLE_SECS` | `--session-idle-secs` | 30 | Idle grace before a viewerless session is reaped | | ||
| | `GATEWAY_SESSION_MAX_SECS` | `--session-max-secs` | 1800 | Hard cap on a session's lifetime | | ||
| | `GATEWAY_RESOLUTION` | `--resolution` | `1920x1080` | Capture resolution WxH (render = encode, 1:1) | | ||
| | `GATEWAY_CLIP_BITRATE_KBPS` | `--clip-bitrate-kbps` | `10000` | Clip video bitrate (kbps) | | ||
| | `GATEWAY_CAST_BITRATE_KBPS` | `--cast-bitrate-kbps` | `6000` | Cast video bitrate (kbps) | | ||
| | `GATEWAY_CLIP_ENCODER` | `--clip-encoder` | `h264-sw` | `h264-sw`, or `h264-hw` (Vulkan Video) | | ||
| | `GATEWAY_CAST_ENCODER` | `--cast-encoder` | `vp9-sw` | `vp9-sw`, `av1-sw` (SVT-AV1), or `av1-hw` (NVENC) | | ||
|
|
||
| Capture runs at **1080p30** by default: the page renders **and** encodes at the same resolution (1:1, no downscale), so text stays crisp and the page gets a real desktop layout. Drop to `res=1280x720` for less bandwidth or raise to `res=2560x1440` for more detail (capped at 4K); frame rate is fixed at 30. | ||
|
|
||
| ### Encoders | ||
|
|
||
| Software encoders are the default, so the gateway runs anywhere with no GPU — ideal for local dev. Hardware is opt-in per mode: | ||
|
|
||
| | Profile | Encoder | Notes | | ||
| |---|---|---| | ||
| | `h264-sw` | OpenH264 | clip default; universal | | ||
| | `h264-hw` | Vulkan Video H.264 | NVIDIA/AMD/Intel (NVENC exposes no H.264) | | ||
| | `vp9-sw` | libvpx VP9 | cast default | | ||
| | `av1-sw` | SVT-AV1 | software AV1 — exercise the AV1 path without a GPU | | ||
| | `av1-hw` | NVENC AV1 | NVIDIA; crisper than VP9 for cast | | ||
|
|
||
| Clips stay H.264 so they play everywhere; cast AV1 plays in Chromium/Firefox (the same browsers that already handle the WebM stream). Hardware profiles require the matching runtime in the **backend** (Vulkan for `h264-hw`, CUDA + NVENC for `av1-hw`) — the gateway only selects the node, so a missing runtime surfaces as a backend pipeline error, not a gateway one. | ||
|
|
||
| ## Lifetime & teardown (cast) | ||
|
|
||
| The engine does **not** stop a pipeline when its MSE viewers disconnect, and has no idle reaper — so lifetime is the gateway's job. It keeps **one session per normalized URL** (viewers of the same page share one renderer), refcounts viewers across the streaming proxy, and a background reaper tears down sessions that have been **idle** past the grace window or have exceeded their **max lifetime** (via `DELETE /api/v1/sessions/{id}`, which fully drops the pipeline). On `SIGTERM` it drains and destroys every owned session, so a redeploy never leaks renderers. | ||
|
|
||
| ## Metrics | ||
|
|
||
| Prometheus metrics at `GET /metrics` (not gated by the concurrency limit). Beyond the per-endpoint request/latency/rejection series (`endpoint` is `clip` or `cast`), `cast` adds `webcapture_active_sessions`, `webcapture_active_viewers`, `webcapture_session_lifetime_seconds`, and `webcapture_sessions_reaped_total{reason}`. A public instance may keep `/metrics` inside its trust boundary rather than exposing it. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **Public / URL-only.** Targets that resolve to loopback/private/link-local/CGNAT/cloud-metadata addresses are rejected (SSRF guard). Auth-gated pages (cookies/headers) are intentionally out of scope — a future self-hosted feature, never the public path. DNS rebinding between the gateway's check and Servo's own fetch is a known gap. | ||
| - **Servo is view-only and software-rendered**: static/CSS pages render at full frame rate; heavy WebGL is ~15–20 fps. No clicking, scrolling, or login. | ||
| - **`cast` in Safari/iOS** may not play (WebM/VP9), see above. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: SPDX/REUSE compliance for headerless config files is satisfied via REUSE.toml
The new
.gitignoreand.golangci.ymllack inline SPDX headers, which would appear to violate the CONTRIBUTING/AGENTS rule that all new files carry SPDX headers. I confirmedREUSE.tomlat the repo root aggregates**/*.yml,.gitignore, and**/.gitignorewith the MPL-2.0 license (and this matches the siblingexamples/speech-gateway/.golangci.yml, which is also headerless). So these files are compliant and I did not report them as rule violations.go.sumis present on disk (just omitted from the diff), so the Dockerfilego buildis not broken.Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
Playground