Repository navigation
perf(preview): remote browser tabs stream video, and hosts can render them on a GPU - #78
Merged
Merged
Conversation
…eued scrolls Streamed browser tabs sent a full JPEG per frame with one frame per round trip, so a viewer behind a relayed or slower link got a few frames per second and seconds of input lag. Each queued wheel also waited for the page in turn, so a scroll kept replaying long after the user stopped. Viewers that decode WebCodecs video now get H.264 (or VP8) frames encoded in a scratch page of the server's Chromium, only when a frame is about to be sent, with a JPEG still once the page settles. Small frames share a bounded in-flight window instead of one per round trip, and wheels and pointer moves waiting behind a slow page merge into one dispatch. Clients without WebCodecs and older clients keep the JPEG stream unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tings Server browser tabs always rasterized on the CPU at 2x, so a host with a GPU still capped scrolling near 30 fps and spent cores drawing pixels. Settings → Browser → Rendering now sets, per environment, the GPU the browser draws with (Software, the default; Automatic; or a GPU found on a Linux host by its PCI path) and the render scale (1x, 1.5x, 2x as the default, 3x). The host lists its GPUs through preview.listGpus. Changing either relaunches that host's Chromium: open tabs tell their viewers to reconnect and reopen at their page. The mobile app sets both from its Threads settings page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a GPU, the still a video viewer gets once the page settles repaints the page, and Chromium sends that identical picture as a new frame. It counted as motion, so another still followed 400 ms later, forever: about 7 Mbit/s on an idle page, and real frames waited behind each still. A frame byte-identical to the previous one is now acked and skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
Server browser tabs streamed to a remote viewer are unusable over a slower link. On the link from araveia to syssec-canis (a Tailscale DERP relay, about 21 Mbit/s with a 17–40 ms RTT median and frequent spikes past 500 ms), a keypress took 0.6–2 s to appear, a scroll trailed the wheel by 2–3 s, and animation reached about 3 fps.
Three things caused it:
Change
I took the parts of the upstream discussion that help this link and left out the parts that don't (decisions below):
VideoDecoder.isConfigSupportedand sendscodecs=avc1.42E034,vp8on the stream URL.Compatibility goes both ways:
0xFF, and video messages start with1(keyframe, carrying codec and size) or2(delta). The format lives inpackages/contracts/src/preview.ts.codecsand get the same JPEG stream as before. New clients on old servers just receive JPEG.The web panel, web picture-in-picture and the mobile WebView all share the
client-runtimestream client and painter, so they all get this. Desktop-rendered tabs and agent recordings don't use this path and are unchanged.Host GPU and render scale
Settings → Browser → Rendering has two new per-environment rows. Mobile has them on the Threads settings page, when no project is selected.
--disable-gpu. Automatic enables the GPU (on Linux, ANGLE over EGL, without which headless Chromium stays on SwiftShader). A specific GPU is offered on Linux hosts.preview.listGpusRPC. It reads/dev/dri/by-path/*-render, sysfs vendor/device/driver andpci.idsnames.--render-node-override.--force-device-scale-factor, the context device scale, zoom emulation and snapshot capture scale.Both are launch flags. A change relaunches that host's Chromium:
docs/user/remote-access.mdsays where to find the settings.Approaches considered and dropped
Scope and approval
This is a performance fix for our fork, at the request of the fork's owner. Upstream would need prior approval, since it changes the default stream format and the frame-rate/bandwidth tradeoff (as pingdotgg#19462 was told).
Verification
Benchmark, araveia → syssec-canis over the real relay.
Setup:
main's) realServerBrowserand/api/preview-streamroute, with real Chromium; only auth and the desktop channel are stubbed. Tailscale serves it over HTTPS on the same relay path the desktop uses.client-runtimestream client and painter bundled from each checkout.mainand 66 ms here, since encoding costs about 11 ms of server time per frame. Scroll is 1482 → 262 ms and animation 14 → 28 fps.GPU and render scale, araveia → syssec-canis. The relay p50 was 15–28 ms. Each cell gives the range over 2 alternating rounds; everything is on this branch:
--render-node-override=/dev/dri/by-path/pci-0000:2b:00.0-render --force-device-scale-factor=1, and WebGL reported the NVIDIA renderer.Tests. These fail on
mainand pass here:ServerBrowserStream.test.ts: small frames share the window, frames that wait are encoded only when sent, and the existing large-frame case still holds. Key frame requests and thecodecsparameter are covered too.ServerBrowser.test.ts: wheels and moves waiting behind a slow page merge into one dispatch, and a key keeps its place.preview.test.ts: the frame header round-trips, and JPEG is told apart from video.ServerBrowser.test.ts: a rendering change relaunches the browser and the tab reopens at its last URL (fails without the re-navigation). An identical screencast frame is skipped (fails without the skip).ServerBrowserContexts.test.ts:relaunchcloses every browser, and the next launch uses the current flags and scale.ServerBrowserGpus.test.ts: discovery against a fake sysfs/devfs/pci.ids tree, plus the flag mapping, including a missing GPU and an injected flag.vp run check:changedpasses.Not checked:
client-runtimecode. Electron and Android WebView are Chromium-based too.VideoDecoderhere is unverified. Without it, iOS stays on JPEG.Model: Claude Opus 5.5. Harness: Claude Code through T3 Code.
🤖 Generated with Claude Code