jkunz 9d6e6ec374
Paint Chromium / paint-chromium (push) Failing after 1h0m54s
feat(paint): build and publish the Paint Chromium release in CI
`.smartconfig.json` names the Gitea release paint-chromium-<build>. The
Paint Chromium workflow builds it in capped containers on the runner's
Docker daemon and publishes it with the job token unless it is already
published. Package releases install the published descriptor in their
preflight instead of rebuilding the browser.
2026-10-09 16:43:18 +00:00

@push.rocks/smartbrowser

A Chromium automation package for screenshots, page evaluation, shadow DOM server rendering, live browser sessions, managed browser resources, and browser-side rendering.

The browser-side entry at @push.rocks/smartbrowser/web provides LiveBrowserVideoRenderer, LiveBrowserCanvasRenderer, the opt-in LiveBrowserPaintRenderer, the viewer-owned audio controller and byte mount, and DevToolsFrontend without bundling Puppeteer or Node.js code into the web application.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

Install the package with pnpm:

pnpm add @push.rocks/smartbrowser

The root, /automation, /ssr, and /browser entries require Node.js 22.12 or newer and a Chromium-compatible browser. The managed /runtime and /mcp entries require Node.js 24 through 26 on a non-root Linux x64 host with sandbox-capable Chromium. The package uses Puppeteer 25 and keeps native WebRTC support through @push.rocks/smartwebrtc.

Entry Purpose
@push.rocks/smartbrowser SmartBrowser screenshots and page evaluation, plus the complete browser API
@push.rocks/smartbrowser/automation Browser launch, executable discovery, IncognitoBrowser, and Puppeteer without loading live-session or managed-runtime modules
@push.rocks/smartbrowser/ssr SmartSSR shadow DOM prerendering through an isolated Chromium context
@push.rocks/smartbrowser/browser LiveBrowserSession, CDP, video, DevTools, and their contracts
@push.rocks/smartbrowser/runtime Resource ownership, capabilities, leases, confinement, and bounded artifacts
@push.rocks/smartbrowser/mcp Authenticated MCP HTTP handler for the managed runtime
@push.rocks/smartbrowser/web Canvas, video, and opt-in paint renderers, viewer audio controller/client/byte mount, and the embedded DevTools frontend

The /automation and /ssr imports avoid loading the live-session and native-video modules. Installing the package still installs its declared dependencies.

Usage

@push.rocks/smartbrowser provides a high-level SmartBrowser class for screenshots and page evaluation.

Getting Started

Import and initialize a SmartBrowser instance:

import { SmartBrowser } from '@push.rocks/smartbrowser';

const smartBrowser = new SmartBrowser();
await smartBrowser.start();

Capturing a Screenshot of a Webpage

Capture a PNG screenshot of any webpage:

const screenshotResult = await smartBrowser.screenshotFromPage('https://example.com');
console.log(screenshotResult.buffer); // Screenshot buffer (PNG)
console.log(screenshotResult.name);   // Short unique identifier
console.log(screenshotResult.id);     // Identifier with extension

Evaluating JavaScript on a Webpage

Run arbitrary JavaScript inside a page context and retrieve the result:

const pageTitle = await smartBrowser.evaluateOnPage('https://example.com', async () => {
  return document.title;
});
console.log(pageTitle); // "Example Domain"

The evaluateOnPage method supports generic return types:

const metrics = await smartBrowser.evaluateOnPage<{ width: number; height: number }>(
  'https://example.com',
  async () => {
    return {
      width: window.innerWidth,
      height: window.innerHeight,
    };
  }
);
console.log(metrics.width, metrics.height);

Pages are automatically closed after evaluation, even if an error occurs.

Accessing the Underlying Puppeteer Browser

For advanced use cases, you can access the Puppeteer browser instance directly:

const page = await smartBrowser.headlessBrowser.newPage();
await page.goto('https://example.com');
// ... custom Puppeteer operations
await page.close();

For browser launch without loading live-session modules, use /automation:

import { getEnvAwareBrowserInstance } from '@push.rocks/smartbrowser/automation';

const browser = await getEnvAwareBrowserInstance();
try {
  // Use Puppeteer pages here.
} finally {
  await browser.close();
}

Upgrading from 5.x

  • Runtime state carries presentation. IBrowserRuntimeState may include presentation, the per-incarnation capability report: binary profile, compositing, and Paint, audio and DevTools availability. validateAgentActionResult checks it exactly; an unknown field or an out-of-range value is a PROTOCOL_ERROR. Code that relays runtime state must pass the field through unchanged.
  • One presentation per lease. A lease presents Paint or Video, never both: subscribePaint() while a video peer may exist, or openVideoPeer() while Paint is open, rejects with PRESENTATION_BUSY. Move between modes with lease.switchPresentation({ to: 'paint', paint } | { to: 'video' } | { to: 'none' }).
  • @push.rocks/smartipc 3.x. SmartBrowser now depends on smartipc 3.x (the runtime's cross-process ownership lock and the Paint launch-status channel). Align any direct smartipc dependency of your own to 3.x.

Migrating from SmartPuppeteer and browser-runtime

Import browser launch and live-session APIs from this package:

import { getEnvAwareBrowserInstance } from '@push.rocks/smartbrowser/automation';
import { LiveBrowserSession } from '@push.rocks/smartbrowser/browser';
import { BrowserRuntime } from '@push.rocks/smartbrowser/runtime';
import { createBrowserRuntimeMcpHttpHandler } from '@push.rocks/smartbrowser/mcp';

The old smartpuppeteer namespace export from the SmartBrowser root entry is removed; its functions and types are direct root exports or available from /browser. Managed runtime imports previously taken from @modelprofile.com/browser-runtime move to /runtime and /mcp.

Migrating from SmartSSR

Import SmartSSR from the isolated /ssr entry. The constructor still accepts a debug flag (default false), and renderPage(url) still resolves to the rendered HTML string. It launches an isolated browser context, waits for networkidle2, captures the page content after flattening open shadow DOM, and enforces a 30-second render deadline. A failed navigation or serialization now rejects with its error, and the browser is closed before the call settles.

import { SmartSSR } from '@push.rocks/smartbrowser/ssr';

const ssr = new SmartSSR();
const html = await ssr.renderPage('https://example.com');

Debug mode supplies the rendered HTML and a PNG screenshot to an application callback after the browser is closed. The callback is required when debug: true; it can be asynchronous and its rejection is passed to the caller. SmartBrowser does not write these artifacts to the filesystem or create a package .nogit directory. Applications decide how to use or persist them.

const debugSsr = new SmartSSR({
  debug: true,
  onDebugArtifacts: async ({ html, screenshot }) => {
    console.log(html.length, screenshot.length);
  },
});
await debugSsr.renderPage('https://example.com');

PDF operations remain in @push.rocks/smartpdf. The former SmartBrowser.smartpdf field and SmartBrowser.pdfFromPage() method are removed. Use SmartPdf.getFullWebsiteAsSinglePdf() for the same full-page PDF result, including text-extraction metadata. SmartPdf.start() can launch its own browser or accept one borrowed from /automation:

import { SmartPdf } from '@push.rocks/smartpdf';
import { getEnvAwareBrowserInstance } from '@push.rocks/smartbrowser/automation';

const browser = await getEnvAwareBrowserInstance();
const pdf = new SmartPdf();
try {
  await pdf.start(browser);
  const result = await pdf.getFullWebsiteAsSinglePdf('https://example.com');
  console.log(result.buffer, result.metadata?.textExtraction);
} finally {
  await pdf.stop();
  await browser.close();
}

Embedded Chrome DevTools

The backend export getDevToolsFrontendAssets() supplies serveDirectory, urlPrefix, entrypointUrl, and chromeMajor. Serve the directory at its full URL path under urlPrefix, with GET/HEAD access and same-origin framing enabled. Authenticate the separate CDP transport before admitting commands or delivering events. Keep the main application's framing policy separate. The assets include the official Chrome 154 frontend, lazy panels, translations, and third-party notices; no external DevTools website or debugging port is required.

import { DevToolsFrontend } from '@push.rocks/smartbrowser/web';

export async function mountDevTools(
  iframe: HTMLIFrameElement,
  entrypointUrl: string,
  send: (message: string) => Promise<void>,
  onClose: (reason: string) => void,
) {
  const frontend = new DevToolsFrontend({ iframe, entrypointUrl, send, onClose });
  await frontend.start();
  frontend.showPanel('console');
  return frontend;
}

Connect send to a selected-tab LiveBrowserSession.openDevTools() connection through your authenticated transport. Resolve send on bounded transport admission, before CDP execution finishes. Execute backend commands concurrently so Debugger.resume can run while an evaluation is paused. Feed every backend message to await frontend.dispatch(message) in order, including messages received during start(). Await dispatch before reading the next message to preserve backpressure. The protocol permits 1 MiB commands and 8 MiB responses/events. Oversized messages, stalled receivers, and invalid handshakes close the inspector explicitly.

showPanel accepts elements, console, network, or sources. close() releases the MessagePort, rejects pending deliveries, and clears the iframe. Applications must also close the backend attachment on view revocation, tab replacement, or transport loss. The iframe handshake is bound to its origin, window, and per-mount nonce; this does not replace backend authorization. Browser-wide operations and native host features remain subject to the managed browser's policy.

The pinned revision and SHA-256 resource manifest are shipped beside the frontend. To rebuild from official Chromium sources, run node scripts/build.devtools.mjs from the repository. Set SMARTBROWSER_DEVTOOLS_BUILD_CACHE to a reusable directory outside the package tree if needed. The script pins Chromium's frontend and depot_tools revisions, runs the upstream GN/Ninja build in a resource-capped container, and collects its shipping manifest. Building the package or installing it does not download a build toolchain. Frontend preferences use the smartbrowser.devtools. storage prefix.

Website exceptions arrive through renderer onError with code remote_page_error and an optional tabId. Display them in the inspected website's context; they do not indicate that the browser transport failed. Runtime failures retain remote_browser_error.

Live Browser Video

LiveBrowserVideoRenderer presents a SmartBrowser video stream directly in an HTMLVideoElement. It shares the canvas renderer's bounded input queue, held-input cleanup, viewport negotiation, and suspend/resume lifecycle. There is no JavaScript image decoding or canvas copy in the video path.

import { LiveBrowserVideoRenderer, type ILiveBrowserVideoClient } from '@push.rocks/smartbrowser/web';

export async function mountVideo(video: HTMLVideoElement, client: ILiveBrowserVideoClient) {
  const renderer = new LiveBrowserVideoRenderer({ video, client, resizeTarget: video.parentElement! });
  await renderer.start();
  return renderer;
}

The client provides the same state and input operations as ILiveBrowserCanvasClient, replacing frame acknowledgements with openVideoPeer(options), answerVideoPeer(negotiationId, description, options), closeVideoPeer(options), and getVideoStatistics(options). Every asynchronous operation receives an abort signal and must settle promptly when cancelled. The host authenticates and owns the peer through the viewer's lease; callers do not choose peer IDs or ICE credentials. Subscribe to state/error events without JPEG frames. Keep the signaling transport alive until stop() completes, and close the host lease even when transport cleanup fails.

start() installs listeners and begins negotiation without waiting for video, allowing applications to activate their signaling transport afterward. Input remains blocked until the browser presents a frame for the exact active tab, stream generation and viewport revision. Source changes clear the old picture immediately. suspend()/resume() close and negotiate a fresh peer. Hiding the document releases its peer; showing it opens a new one. Native video uses muted autoplay, inline playback, and object-fit: contain; pointer coordinates exclude the surrounding letterbox.

ICE configuration comes from the host offer. With the default empty ICE server list, media connects directly over a reachable LAN or VPN without a relay or external STUN service. Signaling continues to use the application's authenticated transport. The receiver requests the lowest supported jitter-buffer target; the browser retains its network-dependent minimum. Chromium owns decoding, congestion control and frame scheduling.

The Rust/NVENC backend also supplies state.videoSource. Forward that object intact: its rtpTimestampFloor prevents buffered frames from a previous viewport or navigation from enabling input, including when the dimensions have not changed. Odd native dimensions use frameAlignment: 2; GPU CSS compositing crops the encoder's alignment pixel and centers the visible viewport. Pointer mapping uses that same rectangle through resizes. No frame pixels pass through JavaScript. Mouse input includes the original DOM timestampMs; transport adapters must preserve it, while synthesized releases omit it.

getStatistics() includes the existing cumulative input/frame counters and an optional video snapshot: connection state, received bytes, bitrate, decoded/dropped frames, FPS, round-trip time, jitter-buffer delay, codec, decoder, selected candidate type and host encoder statistics when available. Sampling is sequential at one-second intervals. Hardware acceleration is reported only when the browser supplies it. Media negotiation and connection failures use video_negotiation_failed / video_connection_failed; applications should recover or close the view explicitly. There is no automatic JPEG fallback.

Live Browser Canvas

Import the browser-only renderer from @push.rocks/smartbrowser/web. The Node.js root entry is intentionally separate and must not be imported into a frontend bundle.

The renderer receives a caller-provided ILiveBrowserCanvasClient. An application adapter implements that interface using its authenticated transport and keeps the latest ILiveBrowserState available through getState(). The adapter must emit frame events with strictly increasing frame.sequence values for each onEvent() subscription/renderer run. SmartBrowser does not prescribe TypedSocket, WebSocket framing, base64 conversion, authentication, authorization, or session ownership.

import {
  LiveBrowserCanvasRenderer,
  type ILiveBrowserCanvasClient,
} from '@push.rocks/smartbrowser/web';

export async function mountLiveBrowser(client: ILiveBrowserCanvasClient) {
  const viewport = document.querySelector<HTMLElement>('[data-live-browser-viewport]')!;
  const canvas = viewport.querySelector<HTMLCanvasElement>('canvas')!;

  const renderer = new LiveBrowserCanvasRenderer({
    canvas,
    client,
    // Observe a stable CSS-sized element, not the canvas backing bitmap.
    resizeTarget: viewport,
    onError: (error) => console.error(error.code, error.message),
  });

  await renderer.start();
  return async () => {
    // Stop the renderer before closing its transport so cancellation reaches the adapter.
    await renderer.stop();
  };
}

A minimal host keeps CSS sizing independent from the canvas's encoded backing dimensions:

<div data-live-browser-viewport style="width: 100%; height: 600px; overflow: hidden">
  <canvas style="display: block; width: 100%; height: 100%"></canvas>
</div>

ILiveBrowserCanvasClient exposes the renderer-facing subset of the canonical SmartBrowser live-session API:

  • Cached state and events: getState() and onEvent()
  • Frame flow control: acknowledgeFrame()
  • Viewport synchronization: setViewport()
  • Input: dispatchMouse(), dispatchWheel(), dispatchKey(), and insertText()

Every asynchronous client method receives a required second ILiveBrowserCanvasOperationOptions argument containing signal: AbortSignal. Adapters must pass that signal through to their transport operation and reject promptly when it aborts. The renderer aborts it at the operation deadline and when the current run is suspended or stopped. There is no compatibility path for adapters that omit cancellation.

The renderer keeps at most one frame decoding and one newest frame queued. For an active run it validates the complete static frame protocol before requiring frame.sequence to be strictly greater than that run's high-water sequence. Duplicate or out-of-order frames are terminal frame_render_failed protocol errors and are not acknowledged. A resumed run creates a new subscription and resets the high-water sequence by design.

Every frame that passes validation is acknowledged immediately at receipt, before it is decoded, so upstream flow control no longer waits for local decode and presentation. Only the newest queued frame is decoded; a frame superseded while another frame decodes is skipped without decoding. The newest decoded bitmap is presented once per animation frame (a 16 ms timer stands in while the document is hidden), and the canvas backing store is reallocated only when the frame dimensions change. On a canvas without an existing rendering context the renderer claims an ImageBitmapRenderingContext and presents frames with transferFromImageBitmap(); a canvas that already owns a 2D context keeps the 2D drawing path, which is also what applications need when they read pixels back from the canvas.

The renderer makes exactly one acknowledgement attempt for each valid frame, at receipt, while its run remains active; a frame that is later dropped, found stale, superseded, or fails decoding is not acknowledged again. The browser session may fulfill an acknowledgement with { accepted: false } for stale, retired, duplicate, or identity-mismatched frames. That result settles the renderer's local attempt without suspending or retrying, and it does not prove upstream retirement. Only an acknowledgement operation that throws, rejects, or times out suspends the run; reaching acknowledgement capacity does the same. Input identity always comes from the frame actually displayed, so tab, generation, viewport, and active-stream changes block stale clicks and keystrokes. A rejected non-timeout input dispatch clears the ambiguous frame and requires a newly rendered matching frame; a timeout suspends the run and requires explicit resume plus a fresh frame. Encoded frame.width and frame.height set the canvas backing bitmap, while pointer coordinates map through the displayed canvas rectangle into the frame's logical CSS viewport.

Call suspend() when the adapter observes a transport interruption, then call resume() after the adapter has established a fresh transport. Suspension aborts active work, removes listeners, drops queued input and pressed-input recovery, and clears state and the displayed frame. resume() creates a new renderer run generation, re-subscribes to events, reads state again, and keeps input blocked until a new matching frame is displayed. Old input and release transitions are never replayed into the new generation. Operation timeouts request this same suspended lifecycle, so one transient acknowledgement timeout does not permanently poison the renderer instance.

Input commands are dispatched in queue order with up to four commands in flight. Wheel input is sent immediately when capacity exists. While all slots are occupied, adjacent wheel events with the same document, viewport and modifiers accumulate their deltas; completion of an input operation releases capacity without waiting for an animation frame. Later input flushes accumulated wheel deltas first. Adjacent unpressed pointer moves may coalesce to the newest position; held-button movements remain discrete so drag paths are preserved. Under pressure only queued hover positions may be discarded. Wheel deltas never cross an input or modifier barrier. The queue allows 128 discrete commands and 32 coalescable commands. When no eligible hover can free a full queue, the renderer reports input_queue_capacity_exceeded and rejects the new command.

Malformed frame identities, metadata, viewport values, formats, MIME pairings, dimensions, pixel areas, byte lengths, and deterministic image decode or dimension-integrity failures are terminal to the current run. Decoded dimensions are checked before supersession and currentness, so a mismatched decoded bitmap remains terminal even when a newer frame arrived during decoding. These failures are reported as frame_render_failed and require an explicit resume(); the renderer does not retry them automatically. The renderer only schedules rendering, acknowledgements, viewport synchronization, and direct input. Navigation scheduling remains the responsibility of the server runtime and application adapter.

The renderer's fixed internal protocol ceilings are 16 pending frame acknowledgement operations; 4 in-flight input commands, 128 queued discrete input commands, and 32 queued coalescable input commands; frame dimensions of at most 12,288 on either axis and 8,294,400 pixels; encoded frame data of at most 34,226,176 bytes; and viewports of at most 4,096 x 4,096 CSS pixels, device scale factor 3, and 8,294,400 physical pixels (ceil(width * deviceScaleFactor) * ceil(height * deviceScaleFactor)).

Browser-native createImageBitmap() work is not abortable. After a wrapper timeout or run interruption, the renderer closes a bitmap that resolves late and waits for all prior raw decode jobs to settle before start() or resume() starts another run. Consequently, start() or resume() can remain pending indefinitely if the browser platform never settles a prior createImageBitmap() call.

Pointer and wheel input are captured from the canvas. Keyboard and best-effort compositionend input are captured from focusTarget, which defaults to the canvas and receives focus on pointer down. The renderer temporarily makes an unfocusable focus target focusable and restores its prior tabindex on stop. Applications with a dedicated text or IME control can call renderer.insertText(text) explicitly.

When resizeTarget is supplied, resize updates are deduplicated, serialized, and fenced by viewport revision. Sizes observed through ResizeObserver are debounced for about 100 ms (trailing), so a drag resize produces one setViewport() call for the final size; syncViewport() and window resize events measure immediately. Positive fractional dimensions are rounded to at least one CSS pixel, oversized dimensions are reduced proportionally to the 4,096 x 4,096 ceiling, and the device scale factor is clamped to 0.25 through 3 before being reduced further when necessary to stay within the physical-pixel ceiling. The target must have stable CSS dimensions that do not depend on canvas.width or canvas.height; this prevents intrinsic canvas updates from causing resize feedback.

Shared browser hosts may return { viewport, viewportRevision } from setViewport() to acknowledge the effective viewport after combining viewer preferences. The renderer accepts a smaller viewport or an unchanged revision, remembers its own requested size separately, and enables input only when a current displayed frame matches the accepted revision. A frame received before the viewport response can satisfy that fence. A newer authoritative viewport revision supersedes a pending shared resize. Existing clients returning void retain the single-view contract: apply the requested size and advance the viewport revision. Input remains blocked while the request settles; a still-current image stays visible when the aggregate size does not change.

ILiveBrowserCanvasRendererOptions supports:

  • canvas and client: required rendering and transport-adapter dependencies.
  • focusTarget: optional keyboard and composition event target; defaults to canvas.
  • resizeTarget: optional stable CSS-sized element observed for remote viewport updates.
  • getDeviceScaleFactor: optional scale provider; defaults to window.devicePixelRatio and is useful when the application controls remote scaling explicitly.
  • operationTimeoutMs: deadline for client acknowledgements, viewport updates, and input operations; defaults to 10 seconds. Reaching it aborts the operation signal and suspends the run.
  • frameDecodeTimeoutMs: image decode wrapper deadline; defaults to operationTimeoutMs. Reaching it suspends the run and closes the decoded bitmap if it completes late, but cannot force the browser's native decode job to settle.
  • onError: receives typed ILiveBrowserCanvasError values without interrupting renderer cleanup.
  • onFrameRendered: called after a current frame has been drawn.

The renderer exposes start(), suspend(), resume(), stop(), insertText(), syncViewport(), getStatistics(), and the isRunning and isSuspended getters. getStatistics() returns an ILiveBrowserCanvasRendererStatistics snapshot with cumulative framesReceived, framesDecoded, framesSkipped, inputCommandsEnqueued (every accepted input submission, including ones later merged), inputCommandsCoalesced (submissions merged into another command or dropped under pressure), the live inputCommandsInFlight, and lastInputRoundTripMs for the most recently completed input dispatch. lastInputQueueMs reports its local queue wait and lastInputTotalMs includes that wait through the operation reply. These values do not measure visible feedback. start() begins a stopped renderer, while resume() is required for a suspended renderer. syncViewport() requests a fresh measurement of the configured resizeTarget; it is a no-op when no target is configured or the renderer is not running. An explicitly stopped renderer can be started again. The /web entry also exports ILiveBrowserCanvasClient, ILiveBrowserCanvasOperationOptions, ILiveBrowserCanvasRendererOptions, ILiveBrowserCanvasRendererStatistics, ILiveBrowserCanvasError, TLiveBrowserCanvasErrorCode, and the canonical SmartBrowser live-browser contract types.

LiveBrowserCanvasRenderer is not a security boundary. The application adapter must authenticate viewers, authorize control, restrict navigation, enforce browser-session ownership, and apply network/egress policy before forwarding commands. The renderer displays webpage viewport pixels only. It does not provide native Chrome UI, audio, extensions, file transfer, clipboard, camera, microphone, or touch emulation.

Shutting Down

Always stop the browser instance when done to free resources:

await smartBrowser.stop();

This closes the owned Puppeteer browser.

Full Example

import { SmartBrowser } from '@push.rocks/smartbrowser';

async function main() {
  const smartBrowser = new SmartBrowser();
  await smartBrowser.start();

  // Take a screenshot
  const screenshot = await smartBrowser.screenshotFromPage('https://example.com');
  console.log('Screenshot size:', screenshot.buffer.length, 'bytes');

  // Evaluate JavaScript
  const title = await smartBrowser.evaluateOnPage('https://example.com', async () => {
    return document.title;
  });
  console.log('Page title:', title);

  await smartBrowser.stop();
}

main();

Native video retains its peer across document navigation and viewport resizing. Input waits for current document state and a video presentation matching the accepted viewport proportions. The native host replaces capture tracks when dimensions change, preserving the negotiated connection. Pending page dialogs block renderer input; the embedding UI owns the dialog response. A client can reject obsolete input with an Error whose code is stale_input; the renderer retires that obsolete input without treating navigation as a connection failure. dialog_pending retains held keys and buttons, and their release is sent after the dialog closes.

Video statistics sample the receiver independently of remote sender requests. presentedFramesPerSecond measures frames submitted to the compositor; framesPerSecond retains the WebRTC decoded-frame rate. sampledAt uses the viewer's performance.now() clock, allowing consumers to distinguish an old sample from a fresh zero. Optional captureToDisplayMs estimates capture-to-presentation latency from WebRTC frame metadata, while frameAgeMs reports the age of the latest presented capture. Interval measurements include decodeMs, processingMs, jitterBufferMs, jitterBufferTargetMs, and jitterBufferMinimumMs. These measurements can overlap and should not be summed. Network roundTripMs is separate from display latency. Codec, transport protocol, and sender acceleration information remain available.

Live Browser Paint (opt-in)

LiveBrowserPaintRenderer shares the existing live browser control client, pointer mapping, input queue, state subscription, viewport negotiation, and stop/suspend/resume lifecycle. Its separate LiveBrowserPaintPictureRenderer replays native Skia pictures in a CanvasKit WebGL2 worker and transfers the resulting ImageBitmap to a bitmaprenderer canvas. The existing LiveBrowserVideoRenderer remains available for the same session. Paint requires a compatible, separately built native Chromium capture artifact; installing this npm package does not build or download Chromium.

Native Chromium and CanvasKit must use the same canonical Skia custom-picture patch bytes. Their picturePatchSha256 values must match in the packaged native descriptor and frontend build.json; a matching Skia revision alone does not establish picture-stream compatibility. The viewer Paint handshake and every frame carry that hash, which the viewer worker checks against its verified CanvasKit manifest. The native archive retains the canonical patch under source-patches/ and the package descriptor binds its exact hash.

On Ubuntu 23.10 and later, including Ubuntu 24.04, a sandboxed Paint Chromium build in a caller-owned code-asset cache may need a host AppArmor rule allowing user namespaces for its exact executable path. The resolver launches <cacheDirectory>/paint-<archive SHA-256>/chrome-linux64/chrome; cacheDirectory is supplied by the host as an absolute, normalized, private directory. After fixing that directory for a deployment, a platform administrator can assess a path-scoped attachment such as <cacheDirectory>/paint-*/chrome-linux64/chrome with Chromium's documented userns permission. Use one wildcard for the hash-qualified directory, not a recursive match over the cache. Installing and loading the profile is a host/platform action, outside this package. A user who can write a matching executable under that cache can obtain the same permission; verification of the archive does not constrain AppArmor's path match. A root-owned Chrome installation or matching setuid sandbox helper requires separate platform provisioning; the Paint resolver neither selects arbitrary installed Chrome nor installs a setuid helper. See Chromium's AppArmor guidance.

For Paint, create createPaintFrontendAssetResponder({ mountBasePath: '/paint', expectedNativeIdentity }) from the Node/browser entry point and mount its resolve({ method, pathname, ifNoneMatch }) result on the viewer origin. Pass <origin><responder.routePrefix> as assetBaseUrl and <origin><responder.routePrefix>dist_ts_web/classes.paintpictureworkercore.js as workerCoreUrl. The responder verifies package provenance and captures the exact build.json, CanvasKit JS/WASM, compiled worker core, and compiled byte-budget helper bytes once, within a 32 MiB executable snapshot budget. Its separate routeKey commits all five files; assets.buildKey remains the native/CanvasKit build identity. Serve these files only through the responder, never through a mutable raw-directory route. Keep old responders and prefixes available while their viewer leases can resume. Successful GET and HEAD responses use immutable caching and ETags, so a warm view need not download the WASM again; If-None-Match accepts weak tags, lists, and *. Missing assets and unsupported methods return Cache-Control: no-store, preserving old URLs if a responder is temporarily absent. The renderer's JS/WASM hashes detect corruption early; the responder pins the bytes subsequently loaded at those URLs. Package source-deps.json and third-party notices remain packaged for provenance and distribution, outside the executable route. The verified classic worker entry loads the same-origin worker core. Paint renderer options require an explicit positive safe-integer maxAggregateBytes; the Paint worker uses this logical reservation for known cache, SKP, surface, and bitmap costs; the private video decoder budget accepts the same reservation object when a scene consumer is composed. It is not a measurement or hard cap of Skia, GPU-driver, WebCodecs-backend, or garbage-collected memory. A CSP can retain script-src 'self' 'wasm-unsafe-eval', worker-src 'self' blob:, and connect-src 'self'; JavaScript unsafe-eval, a CDN, and script-src blob: are not needed.

The separate native audio executable closure uses createAudioFrontendAssetResponder({ mountBasePath: '/audio' }) from the Node/browser entry point. Mount its resolve({ method, pathname, ifNoneMatch }) result at /audio/<buildKey>/... on the viewer origin, forwarding its status, headers, and body without a second raw-directory route. The responder verifies and pins the allowlisted build.json, decoder MJS/WASM, and frontend ESM files once, then serves copies with immutable caching and ETags on 200/304 responses. Its If-None-Match handling and no-store 404/405 responses follow the Paint responder contract. This same-origin immutable route binds the bytes subsequently executed by module Workers and AudioWorklet; browser-side hash preflights are early corruption checks, not proof that a separate URL load executed the fetched bytes. Audio requires script-src 'self' 'wasm-unsafe-eval', worker-src 'self', and connect-src 'self'; no script-src blob: or eval permission is added for audio.

For a human viewer, authorize one pair of inverse TypedRequest virtual streams on that viewer's current BrowserRuntimeLease. On the server, pass those streams, the lease, and the same trusted sampleCreditBytes/sampleCreditEvents grant to attachNativeMediaAudioByteHost; on the viewer, give the returned pair and grant to NativeMediaAudioByteMount, then wrap it in NativeMediaAudioViewerClient. The pair is single-use for one lease incarnation; a new viewer or revoked lease needs a new authorized pair. The host keeps delivery acknowledgements separate from native sample-credit release and closes both streams and the native source when authority ends. The viewer creates one NativeMediaAudioViewerController with that client, the responder's content-addressed asset URLs and hashes (toNativeMediaAudioDecoderAssets(responder, origin) builds them), and a clockPolicy provider: pass createMeasuredNativeMediaAudioSyncPolicy. The controller calls it at every clock schedule with its own AudioContext's measured sampleRate, baseLatency and outputLatency and the timer quantum, so the policy is derived per host and session; there is no static default. A provider that throws, or returns limits outside the controller's bounds, fails the run with NativeMediaAudioClockPolicyError. Every derived limit is documented at the helper: the 90 ms sync tolerance (ITU-R BT.1359-1 audio-lead acceptability), native's 50 ms playback-state republish, the 1 s clock re-probe (nativeMediaAudioClockProbeRefreshUs; a long-running source re-probes before its calibration expires), the 1000 ppm drift bound and one mixer chunk of lead. Its optional timerResolutionUs is the viewer's real performance.now() resolution in microseconds: Chromium clamps it to 100 µs without cross-origin isolation and to 5 µs with COOP and COEP isolation. Calibration and the clock mapping treat it as the timestamp quantum. It defaults to defaultNativeMediaAudioTimerResolutionUs ('100'), which is correct for every Chromium viewer; pass nativeMediaAudioTimerResolutionUs(globalThis.crossOriginIsolated) for the exact value. A value finer than the document's timer is refused, because it would claim precision the timer lacks. Use defaultNativeMediaAudioSampleCredit for the credit grant on both the host and the viewer. Paint and Video borrow runs from the same controller; interrupt and stop only the matching run, call activate() from a real viewer gesture, and dispose the controller when the viewer ends. Suspending the AudioContext does not block source controls or lease revocation. The browser's public audio entry exports these composition classes and their option types; decoder, admission, timing, and mixer helpers are internal to them.

The codec patent disclosure is assets/__smartbrowser/notices/codec-patents.txt. The Paint Chromium artifact matches branded Linux Chrome's codec configuration: H.264 (including OpenH264 encode for WebRTC), AAC, HEVC through VA-API hardware decode only, AV1 (dav1d, libaom), and the MPEG-2 TS and HLS demuxers; Widevine is off. The viewer audio WASM decodes AAC and Opus. Distributors, including hosts that serve the audio decoder to remote viewers, are responsible for obtaining any patent licences required for their distribution.

createSmartBrowserNoticesResponder({ mountBasePath: '/browser-notices', paintArtifact? }) serves every notice, license, relink object and corresponding source SmartBrowser distributes: the CanvasKit and audio notices, the audio decoder's FFmpeg LGPL relink objects and sources, the DevTools frontend license and notices, the codec disclosure, and, when paintArtifact is installed, the Paint Chromium archive's LICENSE.chromium, THIRD_PARTY_CREDITS.html, FFmpeg notices, ffmpeg-relinking.txt, source patches and provenance. index.json lists every file with its role, license, size and SHA-256, plus the descriptor's corresponding-source offers (the FFmpeg source of libffmpeg.so first); NOTICES.txt aggregates the readable texts. The package inventory assets/__smartbrowser/notices/manifest.json is pinned by SHA-256 in the responder; scripts/build.notices.mjs regenerates it after any notice-bearing asset changes. Files are verified at creation and re-verified on every GET; successful responses are immutable with ETags under a content-addressed key, HTML is served with a sandboxing CSP, and binaries download as attachments.

This software is based in part on the work of the FreeType Team. The Paint distribution includes the FreeType license and this attribution under assets/__smartbrowser/paint/canvaskit/notices/freetype/.

import {
  LiveBrowserPaintRenderer,
  LiveBrowserPaintStreamPresenter,
  type ILiveBrowserCanvasClient,
  type IPaintStreamHello,
  type TPaintStreamTermination,
} from '@push.rocks/smartbrowser/web';

export async function mountPaint(
  canvas: HTMLCanvasElement,
  client: ILiveBrowserCanvasClient,
  assetBaseUrl: string,
  workerCoreUrl: string,
  subscription: { readonly hello: IPaintStreamHello; close(): Promise<void> },
  sendPacket: (packet: Uint8Array, signal: AbortSignal) => Promise<void>,
  onFailure: (error: Error) => void,
  onTermination: (termination: TPaintStreamTermination) => void,
) {
  const renderer = new LiveBrowserPaintRenderer({
    canvas,
    client,
    resizeTarget: canvas.parentElement!,
    picture: {
      assetBaseUrl,
      workerCoreUrl,
      maxPictureBytes: 64 * 1024 * 1024,
      maxResourceBytes: 32 * 1024 * 1024,
      maxCacheBytes: 256 * 1024 * 1024,
      maxCacheEntries: 4096,
      maxOutputPixels: 16 * 1024 * 1024,
    },
  });
  // Attach stream ownership before start can create a worker or fail.
  const presenter = new LiveBrowserPaintStreamPresenter({
    expectedHello: subscription.hello, renderer,
    closeSource: () => subscription.close(),
    sendPacket, onFailure, onTermination,
  });
  try {
    await renderer.start();
    return presenter;
  } catch (error) {
    try { await presenter.close(); }
    catch (cleanupError) {
      throw new AggregateError([error, cleanupError], 'Paint start and subscription close failed');
    }
    throw error;
  }
}

The application serves the assets and negotiates matching byte and entry budgets with its authorized binary paint stream, including maxViewerQueuedBytes for completed frames awaiting replay. The example budgets are application choices, not fixed format limits. Pass the exact subscription.hello from the authorized BrowserRuntimeLease.subscribePaint() response through the application's authenticated control channel. Forward ordered browser-to-viewer binary chunks to presenter.receivePacket(chunk) and send the presenter's upstream chunks through that same authorized viewer-to-browser channel. The application owns the channel and Paint/Video selection; SmartBrowser validates the hello and frame identity, reassembles chunks, orders resource and presentation ACKs, handles granted eviction and revoked frames, and bounds the presentation queue. sendPacket must honor its abort signal. If the control channel supplies a full authoritative scene fence, call presenter.setExpectedFence(fence) when it changes. If the binary channel cannot deliver its typed terminal packet, forward an authenticated runtime onUnsupported or onFailure status with that subscription's hello.source to presenter.terminateFromControl(source, termination); stale source identities are ignored. onTermination receives the exact unsupported, failure, or closed category; among terminal events only unsupported is a Video fallback candidate. presenter.close() closes the matching authorized subscription exactly once and stops the renderer, worker, and input listeners. Ordinary suspend and fatal worker loss also close that subscription. Await closure before resubscribing; the runtime assigns every new subscription a fresh resourceCacheGeneration in its HELLO. A fresh presenter and Worker must use that new HELLO rather than resuming an old resource cache.

The Paint Chromium speaks native Paint protocol 4: every capture is a sealed scene (a vector pass table with its statics and same-draw bitmaps), with or without a current original-video pair, and the package descriptor's identity.protocolVersion must be 4. A page that has not drawn yet, or a minimised window, sends nothing. A transient typed unavailable scene keeps the current display and the stream. Persistent unavailability ends Paint with the typed unsupported termination, the Video candidate: when 3 consecutive scenes were reported unsupported, budget or recording-failed, or exceeded the viewer's capacity, and no scene was presented for at least 2 s (paintUnavailableFallbackScenes, paintUnavailableFallbackAfterMs). A presented scene resets the count; other unavailable reasons neither count nor reset it; a page that has not drawn sends nothing and never counts. After a counted scene the subscription asks the browser to capture again every second, so a static page decides too. Every viewer frame is a sealed scene table; a single picture is a scene whose root draws everything. Pass onSceneCaptured to subscribePaint() to observe each scene's measured capture cost (forcedDraw, bitmapPasses, readbackBytes, readbackMicros, recordMicros); it is diagnostic only and never decides a scene.

For Paint, renderer.getStatistics() counts complete stream frames as framesReceived, successful CanvasKit replays as framesDecoded, and scenes discarded before display as framesSkipped. A displayed bitmap whose commit acknowledgement fails remains replayed but has no successful presentation acknowledgement.

Paint's getOutputScale option controls only the viewer's local backing pixels and defaults to window.devicePixelRatio. The worker draws the native scene uniformly into the canvas's CSS content box at that scale, leaving transparent bars when the box and remote viewport have different aspect ratios; pointer input follows the visible content. picture.maxOutputPixels bounds this local surface before allocation. A local size or DPR change replays the one retained committed picture without changing the remote viewport revision or sending another native frame acknowledgement. The shared runtime negotiates native Video's required remote device scale factor of 1 independently of this local Paint output scale.

If a matching Chromium process reports Paint capture unavailable, its browser session remains usable. subscribePaint() reports the validated reason through onUnsupported and rejects startup with PAINT_CAPTURE_UNAVAILABLE; the host may switch that lease to Video.

One runtime, a binary per incarnation, a presentation per lease

A BrowserRuntime with paintArtifact launches each browser resource on the Paint Chromium when it can; that binary serves both Paint and Video. Set videoBrowser: { executablePath: '/opt/google/chrome/chrome', expectedMilestone: 154 } to name the independent browser used for Video only when Paint is not usable. It must be a regular, root-owned executable that is not group- or world-writable; the runtime never picks an executable from PATH for this fallback. When a Paint launch fails with PAINT_SANDBOX_UNAVAILABLE (for example an AppArmor user-namespace restriction), PAINT_ARTIFACT_UNAVAILABLE (download), or PAINT_ARTIFACT_INTEGRITY (descriptor, archive, extracted or installed files do not verify), the runtime records the typed reason and relaunches the same resource on the Video browser after confirmed cleanup. Without videoBrowser the typed Paint error stands.

Integrity failures fall back to Video because the Video browser is an independent, trusted, root-owned binary: a corrupt or tampered Paint artifact cannot affect it, so refusing Video would only deny service. Paint stays loudly unavailable with PAINT_ARTIFACT_INTEGRITY. A previously verified install that fails re-verification is treated as tampering: every incarnation started from it is stopped with a fatal PAINT_ARTIFACT_INTEGRITY event, and the install is deleted once those processes are confirmed gone. A corrupt download is discarded with its staging directory.

Retries are bounded. Download and sandbox failures retry on a later launch with exponential backoff from 60 seconds to one hour (paintRetry: { baseMs, maxMs }) without a limit, so a host fix such as installing the AppArmor profile takes effect without a restart. Integrity failures against the same descriptor stop after three consecutive attempts; getPaintAvailability() then reports retries: 'exhausted' until rearmPaintArtifact() or a new runtime.

Every incarnation's state carries presentation: its binary profile (paint or video-only), Paint availability and reason, audio availability (Paint binary only), and DevTools availability. DevTools inspects only a browser of the bundled frontend's milestone; a different milestone (for example a system Chrome that updated) reports DEVTOOLS_UNAVAILABLE with milestone-mismatch, and Video keeps working. On a Video-only incarnation subscribePaint() and openAudioRelay() reject with PAINT_UNAVAILABLE and the reason in error.detail.

Paint works under both GPU and software compositing: the Paint Chromium records vector passes beside the real draw and takes bitmaps through copy requests, so --disable-gpu and --disable-gpu-compositing (and video.gpu: 'disabled') are accepted. The GPU process always stays sandboxed and out of process: a Paint launch refuses --single-process, --in-process-gpu, --disable-gpu-sandbox and --disable-namespace-sandbox, and every sandbox-required launch refuses --no-sandbox, --disable-setuid-sandbox and --disable-seccomp-filter-sandbox. Each incarnation reports what Chrome actually uses as presentation.compositing (software or gpu, from Chrome's gpu_compositing feature status); the combined live qualification records Video frame rate and latency on the Paint browser in both modes against system-Chrome Video.

Paint and Video are exclusive per lease. openVideoPeer() while Paint is open, or subscribePaint() while a video peer may exist, reject with PRESENTATION_BUSY. lease.switchPresentation({ to: 'paint', paint } | { to: 'video' } | { to: 'none' }) closes the current presentation completely, then opens the target, without restarting the browser; DevTools, the audio relay, pending dialogs, held input, the viewport and the lease itself are unchanged. On a target failure the lease presents nothing and the typed error propagates. On the viewer, LiveBrowserPresentationSwitcher freezes the last frame, stops the old renderer, lets the host open the new mode, and releases the freeze on the first new frame. Paint renderers take an ILiveBrowserPaintClient, which has no raster acknowledgeFrame.

The Paint launch environment never inherits CHROME_DEVEL_SANDBOX: a user-owned Chromium would trust any helper it names. Only paintSandboxHelper: { executablePath }, a root-owned mode 04755 helper whose ancestors are root-owned and not group- or world-writable and which lies outside the runtime directory and the Paint cache, is passed; anything else is SANDBOX_HELPER_INVALID with the failed check. The Paint artifact itself never contains chrome-sandbox.

The launch-status protocol has a narrow sandbox result. If the selected Paint Chromium reports its actual browser-main "No usable sandbox" decision over the authenticated private socket, and SmartBrowser then confirms the owned child and process group are gone, LiveBrowserSession.start() rejects with PaintSandboxUnavailableError (reason: 'unavailable'); BrowserRuntime reports PAINT_SANDBOX_UNAVAILABLE after its own cleanup and relaunches the resource on the configured Video browser. This status does not identify which host policy or sandbox prerequisite failed, and it is distinct from the post-launch PAINT_CAPTURE_UNAVAILABLE response. An absent report or unconfirmed cleanup retains the existing broad failure and runtime fence. The Paint Chromium sends this report from its browser-main sandbox decision; live qualification of a genuine host sandbox failure is still pending. Do not infer this result from Puppeteer stderr or other launch errors.

PaintPictureError.category distinguishes protocol, capability, gpu-lost, and internal failures. Hash mismatch, malformed resources, or missing required resources are protocol failures and must be reported as such. A capability limit, lost WebGL context, or native surface that cannot be represented by the paint codec may select the explicit Video path. The current paint path does not claim pixel-perfect coverage for HDR/wide-gamut images or native video surfaces; those surfaces need a declared compatibility path. The browser proof exercises CanvasKit GPU APIs under SwiftShader; native page/font fidelity and hardware GPU behavior require separate validation.

To reproduce the viewer assets, run scripts/paint/build.canvaskit.sh all with a reusable SMARTBROWSER_PAINT_BUILD_CACHE outside the package tree. The build pins Chromium/Skia, matching Fontations and FreeType inputs, Emscripten, Rust, Bazel, and the patch checksums recorded in build.json. The source dependencies and their exact revisions are in source-deps.json.

The Paint Chromium is built and published by CI, once per build number. .smartconfig.json names it: @push.rocks/smartbrowser.paintChromium.build selects the Gitea release paint-chromium-<build>, which carries chrome-linux64.tar.gz, the FFmpeg corresponding source ffmpeg-source.tar.gz and the descriptor chrome-linux64.json. The Paint Chromium workflow (.gitea/workflows/paint-chromium.yaml) does nothing when that release is published. Otherwise it fetches, patches, compiles, links and packages the pinned Chromium in capped containers on the runner's Docker daemon (scripts/paint/ci.chromium.sh), then publishes the three assets with the job token. A cold build takes about seven hours and runs in one job, so the runner's job timeout must allow it. Package releases never build or upload the browser: their preflight (scripts/paint/release.chromium.py descriptor) installs the published descriptor after checking its identity against this checkout's pinned revisions, reviewed patches and Paint identity, and every asset against its size and SHA-256. A release built from other inputs is refused, not reused: raise the build number whenever the patches, pins or Paint identity change.

The Paint browser tests use Puppeteer's configured Chrome executable. For a clean development checkout, install Puppeteer's managed browser with pnpm exec puppeteer browsers install chrome, or set PUPPETEER_EXECUTABLE_PATH to a compatible installed Chrome executable before running pnpm test. The tests keep their controlled headless and SwiftShader flags in either case.

Host integration checklist (Paint and Video on one runtime)

A host such as a controller UI integrates Paint with this sequence:

  1. Runtime. Create one BrowserRuntime with the following options:

    • paintArtifact: from getPaintBrowserArtifactDescriptor(). A PaintArtifactDescriptorMissingError or { available: false } means the host starts without Paint and states the reason.
    • cacheDirectory: absolute, private and owned.
    • videoBrowser: { executablePath: '/opt/google/chrome/chrome', expectedMilestone: 154 }.
    • paintSandboxHelper: optional.
    • devTools: true: optional.
  2. State. Read each browser's state.presentation:

    • profile (paint | video-only);
    • compositing (software | gpu, as Chrome reports it);
    • paint (available, or unavailable with reason, attempts, nextAttemptAt and retries);
    • audio;
    • devTools (milestone-mismatch with browserVersion and expectedMilestone).

    Runtime-wide Paint state is getPaintAvailability(). Resume exhausted integrity retries with rearmPaintArtifact(). A viewer that receives this report checks it with validateBrowserRuntimePresentation(value) from the web entry: the same exact check the runtime applies, typed as IBrowserRuntimePresentationAvailability, throwing BrowserRuntimePresentationError with the offending field.

  3. Presentation per lease. Open the first mode with lease.subscribePaint() or lease.openVideoPeer(). Move between modes only with lease.switchPresentation({ to }); the other primitive rejects with PRESENTATION_BUSY. Typed refusals carry error.detail:

    • PAINT_UNAVAILABLE (Video-only browser);
    • PAINT_CAPTURE_UNAVAILABLE (capture probe; Video still works on the same lease);
    • DEVTOOLS_UNAVAILABLE;
    • SANDBOX_HELPER_INVALID.
  4. Viewer assets. Mount createPaintFrontendAssetResponder, createAudioFrontendAssetResponder and createSmartBrowserNoticesResponder on the viewer origin. Derive renderer and decoder URLs with toPaintRendererAssetUrls(responder, origin) and toNativeMediaAudioDecoderAssets(responder, origin). Link the notices responder's NOTICES.txt and index.json from the host UI.

  5. Budgets. Start from these defaults:

    • defaultPaintTransportLimits, defaultNativePaintTransportLimits;
    • defaultPaintPictureRendererLimits (viewer);
    • defaultNativeMediaAudioSampleCredit (host and viewer, the same pair);
    • defaultPaintVideoCompanionOptions (host, subscribePaint({ video })) and defaultPaintVideoByteMountLimits (viewer, PaintVideoByteMount), the same byte wire and queue on both ends.

    Credit bounds are nativeMediaMaxSampleCreditBytes/Events and isNativeMediaSampleCredit.

  6. Viewer.

    • LiveBrowserPaintRenderer takes an ILiveBrowserPaintClient, which has no acknowledgeFrame.
    • LiveBrowserPresentationSwitcher freezes, stops, opens and unfreezes across a switch. Its open(mode) returns { firstFrame: renderer.firstFrame, stop } after renderer.start() resolved. Each Video, Paint or canvas renderer run has its own firstFrame: it resolves once, when the run's first frame is on screen, and rejects with LiveBrowserFirstFrameError when the run stops, suspends or fails before it. The error's reason is stopped, suspended, failed or not-started; a failure carries the renderer's failure (as passed to onError) or the Paint stream's termination (as passed to onTermination).
    • NativeMediaAudioViewerController takes a required clockPolicy: pass createMeasuredNativeMediaAudioSyncPolicy. It also takes an optional timerResolutionUs. The default '100' is correct without cross-origin isolation; nativeMediaAudioTimerResolutionUs(globalThis.crossOriginIsolated) gives the exact value, which is 5 µs with COOP+COEP.
  7. Original video companion. Attach PaintVideoByteMount to paintRenderer.pictureRenderer and close it from observePaintSessionEnd. The byte stream content types are paintVideoHostContentType/paintVideoViewerContentType.

    • Host: subscribePaint({ ..., video: { ...defaultPaintVideoCompanionOptions, workerIncarnation, openRoute, onVideoUnavailable, onFailure } }).
    • Viewer: new PaintVideoByteMount({ ...defaultPaintVideoByteMountLimits, routeId, packets, receipts, renderer, signal, onFailure }).
    • Every number derives from a native or Chromium bound and is documented at its definition: one full native sample credit (16 MiB over 509 samples) per receipt owner and byte queue, the one-owner ceiling nativeMediaSampleOwnerMaxBytes before pair bind, the 5 s native pair-bind bound, four sources (a sealed scene binds at most four videos), H.264 level 6.2 and Chromium's DPB, decode and renderer frame counts (defaultH264VideoResourceLimits), and 10 s receipt, stop and worker deadlines.
  8. Content security policy. Allow script-src 'self' 'wasm-unsafe-eval', worker-src 'self' blob: and connect-src 'self'. Call audio activate() from a real user gesture.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
A simplified Puppeteer wrapper for easy automation and testing tasks.
Readme
64 MiB
Languages
TypeScript 86.5%
JavaScript 5.7%
Python 2%
Shell 2%
C++ 1.9%
Other 1.9%