import type { BlitSurface, ConnectionId } from "./types"; /** * Frame-ready callback. Listeners receive only the surface ID; they should * call {@link SurfaceStore.getCanvas} to obtain the shared backing canvas * that already contains the latest rendered frame. */ export type SurfaceFrameCallback = (surfaceId: number) => void; export type SurfaceEventCallback = (surfaces: ReadonlyMap) => void; /** A position in the composited frame's pixel space. */ export interface RemoteSurfacePointer { x: number; y: number; } /** * What another viewer is currently doing to a surface. * * Both kinds at once, because a touchscreen laptop drives a mouse and a * touchscreen together and each is its own mark set — a retire of one must not * erase the other. `pointer` holds at most one point. */ export interface RemoteSurfaceInput { pointer: readonly RemoteSurfacePointer[]; touch: readonly RemoteSurfacePointer[]; } export type RemoteInputKind = "pointer" | "touch"; /** Where the app draws the text under edit, in surface pixels. */ export interface SurfaceCursorRect { x: number; y: number; width: number; height: number; } /** Effective text-input state for one Wayland toplevel. */ export interface SurfaceTextInputState { enabled: boolean; /** `zwp_text_input_v3.content_hint` bitmask. */ hint: number; /** Numeric `zwp_text_input_v3.content_purpose`. */ purpose: number; /** `zwp_text_input_v3.set_cursor_rectangle`, in surface pixels; null until * the app names one. The view parks its hidden IME capture element over * this, so the host's candidate window opens at the app's caret. */ cursorRect: SurfaceCursorRect | null; } /** One state delivery. `requested` is true only for a fresh committed enable. */ export interface SurfaceTextInputEvent extends SurfaceTextInputState { requested: boolean; } /** Cursor artwork announced by the Wayland client for a surface. */ export type SurfaceCursorImage = { kind: "named"; name: string; } | { kind: "hidden"; } | { kind: "custom"; url: string; hotspotX: number; hotspotY: number; width: number; height: number; }; /** Timestamped record of an incoming surface video frame. */ export interface SurfaceFrameSample { /** `performance.now()` when the frame arrived. */ t: number; /** Server CLOCK_MONOTONIC capture timestamp (wrapping u32 ms). */ sourceT: number; /** Microseconds within `sourceT`, when negotiated with the server. */ sourceSubUs?: number; /** Exact integer microsecond PTS submitted to WebCodecs. */ ptsUs?: number; /** Encoded frame payload size in bytes. */ bytes: number; /** Whether this was a keyframe. */ key: boolean; /** `performance.now()` when WebCodecs produced the decoded frame. */ decodeT?: number; /** `performance.now()` after synchronous visible-canvas blits completed. */ presentT?: number; /** Estimated capture-to-receive time after midpoint clock calibration. */ sourceToRecvMs?: number; /** Receive-to-decoder-output time. */ decodeMs?: number; /** Decoder-output-to-visible-canvas time, including playout delay. */ presentMs?: number; /** Estimated capture-to-visible-canvas submission time. */ e2eMs?: number; } export interface ServerClockSample { serverMs: number; clientMidMs: number; rttMs: number; } /** Signed difference between two wrapping u32 millisecond timestamps. */ export declare function wrappingTimestampDelta(a: number, b: number): number; /** Signed source-time delta in ms, including the wire's fractional part. */ export declare function sourceTimestampDelta(a: Pick, b: Pick): number; /** Map a server timestamp to the browser performance timeline. */ export declare function estimateSourceToReceiveMs(sourceMs: number, receiveMs: number, sync: ServerClockSample): number; /** Fixed-capacity insertion-order numeric ring backed by a TypedArray. */ export declare class NumberRing { readonly capacity: number; private readonly values; private start; length: number; constructor(capacity: number); push(value: number): void; time(index: number): number; toArray(): number[]; } /** Struct-of-TypedArrays storage for the surface debug timeline. A compact * numeric token identifies a slot while it moves through decode/presentation, * so the 240–480 Hz receive path creates no sample objects. */ export declare class SurfaceFrameHistory { readonly capacity: number; private readonly times; private readonly sourceTimes; private readonly sourceSubTimes; private readonly pts; private readonly sizes; private readonly keys; private readonly sourceToReceive; private readonly decodeTimes; private readonly decodeDurations; private readonly presentTimes; private readonly presentDurations; private readonly e2eDurations; private readonly generations; private start; length: number; constructor(capacity: number); push(time: number, sourceTime: number, sourceSubTime: number, pts: number, bytes: number, key: boolean, sourceToReceive: number): number; private resolve; private physical; time(logical: number): number; bytes(logical: number): number; isKey(logical: number): boolean; sourceToRecvMs(logical: number): number; decodeMs(logical: number): number; presentMs(logical: number): number; e2eMs(logical: number): number; sourceDelta(later: number, earlier: number): number; markDecoded(token: number, time: number): boolean; markPresented(token: number, time: number): void; toArray(): SurfaceFrameSample[]; } /** Bounded decoder-correlation queue backed by TypedArrays. */ export declare class PendingFrameSamples { readonly capacity: number; private readonly pts; private readonly tokens; length: number; constructor(capacity: number); push(pts: number, token: number): void; takeByPts(pts: number): number; removeToken(token: number): void; private removeAt; } /** Sliding nearest-rank quantile with no per-sample allocations. The FIFO * and sorted views are typed arrays; insertion/removal are bounded memmoves * over at most 480 doubles, replacing an allocated full sort every frame. */ export declare class RollingQuantile { readonly capacity: number; private readonly fifo; private readonly sorted; private head; length: number; constructor(capacity: number); clear(): void; private lowerBound; private removeOldest; push(value: number, window?: number): void; quantile(q: number): number; } export declare class SurfaceStore { private surfaces; /** * Sizes from an S2C_SURFACE_RESIZED that arrived before the matching * S2C_SURFACE_CREATED, replayed by `handleSurfaceCreated`. The server * builds a joining client's replay under the session lock but broadcasts * concurrently, so a live resize can overtake the create. Dropping it * would be permanent: the compositor only emits a resize when the size * changes and nothing re-announces the current one, so the surface would * keep the stale dimensions — and hence a wrong pointer scale — until * the next genuine resize, which for an idle app may be never. */ private _pendingResizes; /** * Surface ids destroyed but not yet re-created. A resize can also trail * its own destroy (the compositor queues native sizes during render and * flushes them after the toplevel is gone), and that straggler must not * be stashed — ids are recycled, so it would be replayed onto whatever * surface claims the id next. */ private _destroyedSurfaceIds; private connectionId; private decoders; private canvases; private frameListeners; private cursorShapes; private cursorImages; private remoteInputs; private textInputs; private encoderNames; private codecStrings; /** Most recent *AV1* codec string announced per surface. The plain * announcement can be an avc1 one from a preference walk the server is * still working through, which says nothing about the AV1 frames in * flight; this remembers the last string that did. */ private av1CodecStrings; private cursorListeners; private remoteInputListeners; private activationListeners; private textInputListeners; private eventListeners; private _diag; private _diagTimer; private _visibilityHandler; private diagnosticsEnabled; /** Whether continuous streams may trade latency for smoother cadence. * Embedders retain the historical smoothing default; the interactive UI * exposes this as a device-local preference and defaults it off. */ private presentationSmoothingEnabled; private _surfaceFrameSamples; private readonly _emptyFrameSamples; /** Timestamps of decoded output frames (for computing output fps). */ private _surfaceOutputSamples; private readonly _emptyOutputSamples; /** Cumulative per-surface drop/error counters. */ private _surfaceDrops; private _surfaceErrors; /** Recent midpoint clock calibrations; the lowest-RTT sample has the * smallest one-way/asymmetry error and is used for source-age estimates. */ private _serverClockSamples; private _serverClock; /** Received samples waiting for the corresponding WebCodecs output. */ private _pendingFrameSamples; /** Transport receive timestamps waiting for the corresponding decoder * output. This is always populated; unlike diagnostic samples it drives * presentation timing. */ private _pendingFrameReceiveTimes; private static readonly FRAME_SAMPLE_MAX; private static readonly OUTPUT_SAMPLE_MAX; private static readonly CLOCK_SAMPLE_MAX; /** Max decoded frames a presenter may hold between rAF ticks while * presenting newest-wins (no scheduling, so depth is pure overflow * slack). */ private static readonly PRESENT_QUEUE_MAX; /** Consecutive continuous arrivals before PTS scheduling engages. Long * enough that a couple of repaints from a click don't trip it, short * enough that it is running well inside the first second of playback. */ private static readonly SMOOTHING_ENGAGE_FRAMES; /** An arrival or PTS gap longer than this ends the current stream * episode: the surface went idle, so the next frame is a fresh * interaction and must paint immediately rather than wait out a * playout margin computed for the previous episode. */ private static readonly STREAM_GAP_MS; /** How much stream the offset distribution covers. Expressed in time * rather than frames so the horizon is the same at 24 and 240 fps — * too short and the schedule chases noise, too long and it responds * sluggishly to a link that genuinely changed (a Wi-Fi roam). */ private static readonly OFFSET_WINDOW_MS; private static readonly OFFSET_WINDOW_MIN; private static readonly OFFSET_WINDOW_MAX; /** Low quantile taken as "the fastest this path goes". Not the strict * minimum: a burst frame is captured later but shipped immediately * behind its predecessor, so its transit genuinely is shorter, and a * minimum would take that one-off as a permanently faster link. A * quantile ignores it for the same reason the high end ignores a single * late frame — no separate clamp or leak rule needed at either end. */ private static readonly FAST_QUANTILE; /** Highest receive samples ignored when choosing the playout point. A * percentage tail is the wrong unit here: at 240 Hz even 2% discards five * frames, enough to hide the leading edge of every Wi-Fi recovery burst. * Ignoring exactly one sample rejects an isolated pause at every rate but * retains any recurring jitter. */ private static readonly PLAYOUT_OUTLIERS; /** A sub-2 ms protocol ping identifies a same-host server even when the UI * itself was opened through a named gateway route. */ private static readonly LOCAL_RTT_MAX_MS; /** Bound the interaction-latency cost of smoothing. The transport is * expected to deliver inside this window; longer outages still collapse * to newest-wins when the burst arrives rather than replaying stale video. */ private static readonly MAX_PLAYOUT_DELAY_MS; /** Fraction of one source interval removed from a shrinking playout margin * per frame. This sheds the full margin in roughly 3.2 seconds at any FPS. */ private static readonly PLAYOUT_SLEW_DOWN_PER_FRAME; /** Fallback display refresh interval before any rAF delta is measured. */ private static readonly DEFAULT_REFRESH_MS; /** Longest rAF delta that counts as a refresh period. Faster positive * cadences have no policy cutoff; longer gaps are stalls/backgrounding. */ private static readonly RAF_DELTA_MAX_MS; /** EWMA of observed rAF intervals — the display's refresh period. Used * to round each frame's due time to the nearest refresh instead of * systematically deferring anything due a hair after this tick. */ private refreshMs; private lastRafMs; /** Per-surface presenter: queues decoded frames and paints them at vsync * via rAF — newest-wins while the surface is idle or interactive, * scheduled against capture-time PTS once it is streaming continuously. * See {@link SurfacePresenter}. */ private presenters; /** * Callback to send a surface ACK to the server. Injected by the * connection layer; each ACK carries the current WebCodecs queue depth so * the server sees decoder pressure without interpreting JS scheduling as * congestion. */ private _ackSender; /** * Callback to request a keyframe from the server (re-subscribe). * Called when the decoder enters an error state and needs a clean * reference point to recover. */ private _keyframeSender; /** Install the ACK sender callback (called once by BlitConnection). */ setAckSender(fn: (surfaceId: number, decoderQueueDepth: number) => void): void; /** Install the keyframe-request callback (called once by BlitConnection). */ setKeyframeSender(fn: (surfaceId: number) => void): void; /** Keyframe requests made while a decoder cannot produce output, per * surface: when the last one went out and how many this episode has cost. * Successful decoded output clears the episode. */ private _unconfiguredRetry; /** Spacing and budget for recovery requests. Each is a SURFACE_SUBSCRIBE on * the wire that forces another keyframe, so this must never become a * per-frame ask; a handful, seconds apart, is enough for a stream whose * configuration is one announcement away, and a stream that stays * unconfigurable stops costing anything after that. */ private static readonly UNCONFIGURED_RETRY_MS; private static readonly UNCONFIGURED_RETRY_MAX; /** * Ask for a keyframe on behalf of a surface whose decoder is dropping * every frame because it cannot configure or decode the stream. * * Without this the drop is silent and terminal: the codec-string * announcement that would configure the decoder only arrives when the * server rebuilds the session, which a healthy encoder has no reason to * do, so the pane stays black for as long as frames keep flowing. The * request re-subscribes, which does rebuild it. */ private retryUnconfigured; /** * WebCodecs codec string to configure an AV1 decoder for `surfaceId` * with. The announced string is authoritative when it describes an AV1 * stream; otherwise the frames themselves are the better evidence — * they say AV1, so a string derived from them beats leaving the decoder * unconfigured and dropping them. */ private av1CodecString; /** Configure `entry`'s AV1 decoder, reporting whether it took. A failure * leaves the decoder closed — it cannot decode and cannot be retried. */ private configureAv1Decoder; /** * Callback to drop codec-support bits and renegotiate the encoder. * Called after a stream fails to decode repeatedly: a fresh keyframe of * the same stream will fail the same way, so re-requesting keyframes * forever just loops on a black pane. */ private _codecDemoter; /** Consecutive decode failures per surface since the last decoded frame, * with the timestamp of the most recent one. */ private _decodeFailStreak; /** Decode-failure episodes tolerated before demoting codec support. */ static readonly DECODE_FAILURES_BEFORE_DEMOTION = 3; /** How long a decode failure keeps counting towards the streak. * * Demotion is for a stream this platform cannot decode at all, which * fails on every keyframe recovery in a row — seconds apart at most. * Without a window, one bad frame an hour still accumulates, so a page * left open all day eventually demotes a codec that works. */ static readonly DECODE_FAILURE_WINDOW_MS = 10000; /** Install the codec-demotion callback (called once by BlitConnection). */ setCodecDemoter(fn: (surfaceId: number, bits: number) => void): void; /** * Record a decode failure; after * {@link DECODE_FAILURES_BEFORE_DEMOTION} in a row (each already a * keyframe-recovery attempt), stop asking for keyframes of a stream this * platform's decoder rejects and demote the codec-support bits that * selected it, so the server renegotiates to a different encoder. The * 4:4:4 flavor goes first when the announced string says that is what we * are being sent; the base codec goes only if failures continue. */ private noteDecodeFailure; private sendAck; /** Send an ACK unconditionally — used by the connection layer's catch * path when handleSurfaceFrame throws before it can ACK itself. */ sendAckFallback(surfaceId: number): void; /** * Monotonically increasing counter bumped on every disconnect. Consumers * (e.g. {@link BlitSurfaceCanvas}) compare their last-seen generation to * detect reconnects and re-subscribe for video frames. */ private _generation; get generation(): number; /** * Whether the browser can decode surface video frames (WebCodecs + secure * context). Checked eagerly at construction time so callers can skip * surface subscriptions that would only drive the server encoder for * nothing (and risk crashing it). */ readonly canDecodeVideo: boolean; /** * Non-null when surface video decoding is unavailable (e.g. insecure * context or missing WebCodecs). UI components should display this * message instead of a blank canvas. */ videoUnavailableReason: string | null; constructor(); onFrame(listener: SurfaceFrameCallback): () => void; onChange(listener: SurfaceEventCallback): () => void; getSurfaces(): ReadonlyMap; /** Debug info about all known surfaces (encoder, codec, size, decode stats). */ getDebugStats(): { surfaceId: number; codec: string; encoder: string; width: number; height: number; /** Typed ring of recent incoming frame samples (for timeline graph). */ frameSamples: SurfaceFrameHistory; /** Ring buffer of decoded-output timestamps (for fps computation). */ outputSamples: NumberRing; /** Cumulative dropped frame count. */ dropped: number; /** Cumulative decode error count. */ errors: number; /** Current WebCodecs decode queue depth. */ queueDepth: number; /** RTT of the midpoint clock sample used for latency estimation. */ clockRttMs: number | null; }[]; getSurface(surfaceId: number): BlitSurface | undefined; /** Return the shared backing canvas for a surface — the server sends * one stream per `(cid, sid)`, so a single decoder and canvas per * surface suffice. The canvas is never attached to the DOM; * callers blit from it into their visible canvases. */ getCanvas(surfaceId: number): HTMLCanvasElement | null; /** Per-frame latency histories are only useful while the debug pane is * visible. Keeping them off otherwise removes diagnostic allocation and * correlation work from the video hot path. */ setDiagnosticsEnabled(enabled: boolean): void; /** Allow or bypass the decoded-frame playout buffer. * * Disabling it is an immediate latency operation: cancel any pending rAF, * discard the learned path margin, and paint the newest queued frame now. * Re-enabling starts with a fresh timing window so an old network stall * cannot become latency in the new smoothing episode. */ setPresentationSmoothingEnabled(enabled: boolean): void; setConnectionId(id: ConnectionId): void; /** Add one server CLOCK_MONOTONIC ↔ performance.now() calibration. * The midpoint estimate assumes a roughly symmetric path; retaining the * lowest-RTT sample bounds queueing error and gives the debug pane an * honest uncertainty indicator. */ noteServerClock(serverMs: number, clientSendMs: number, clientReceiveMs: number): void; clearServerClock(): void; handleSurfaceCreated(surfaceId: number, parentId: number, width: number, height: number, title: string, appId: string): void; handleSurfaceDestroyed(surfaceId: number): void; handleSurfaceFrame(surfaceId: number, timestamp: number, flags: number, width: number, height: number, data: Uint8Array, timestampSubUs?: number): void; handleSurfaceTitle(surfaceId: number, title: string): void; handleSurfaceCursor(surfaceId: number, shape: string, image?: SurfaceCursorImage): void; /** Get the current CSS cursor for a surface. */ getCursor(surfaceId: number): string; /** Get the cursor artwork used for another viewer's pointer overlay. */ getCursorImage(surfaceId: number): SurfaceCursorImage; /** Register a callback for cursor shape changes. Returns unsubscribe fn. */ onCursor(listener: (surfaceId: number, shape: string) => void): () => void; /** * Replace one kind of mark for a surface. An empty `points` retires that kind * and leaves the other alone. */ handleRemoteInput(surfaceId: number, kind: RemoteInputKind, points: readonly RemoteSurfacePointer[]): void; getRemoteInput(surfaceId: number): RemoteSurfaceInput | null; onRemoteInput(listener: (surfaceId: number, input: RemoteSurfaceInput | null) => void): () => void; /** A Wayland client asked for its toplevel to be activated * (xdg_activation_v1 — e.g. a notification click). Not a state change, * so it goes to dedicated listeners, not the change cycle. */ handleSurfaceActivated(surfaceId: number): void; /** Register a callback for surface activation requests. Returns unsubscribe fn. */ onActivated(listener: (surfaceId: number) => void): () => void; handleSurfaceTextInput(surfaceId: number, event: SurfaceTextInputEvent): void; getTextInput(surfaceId: number): SurfaceTextInputState | null; onTextInput(listener: (surfaceId: number, state: SurfaceTextInputEvent) => void): () => void; handleSurfaceEncoder(surfaceId: number, rawPayload: string): void; handleSurfaceAppId(surfaceId: number, appId: string): void; handleSurfaceOrigin(surfaceId: number, sandboxEngine: string, appId: string, instanceId: string): void; handleSurfaceResized(surfaceId: number, width: number, height: number, logicalWidth?: number, logicalHeight?: number): void; /** * Full teardown on transport disconnect. Clears all surfaces, canvases, * and decoders so the UI reflects the disconnected state immediately. * The server's initial message sequence after reconnect * ({@link reset} via S2C_HELLO, then S2C_SURFACE_CREATED) will rebuild * the surface list. The generation counter is bumped so * {@link BlitSurfaceCanvas} instances detect the reconnect and * re-subscribe for video frames. */ handleDisconnect(): void; /** * Full surface reset — called when S2C_HELLO signals a (possibly new) * server instance. Clears all surfaces, canvases, and decoders. The * server's initial message sequence will rebuild the surface list via * individual S2C_SURFACE_CREATED messages. */ reset(): void; /** * Full teardown — only called when the connection is permanently disposed. */ destroy(): void; private clearRemoteInput; private releaseCursorImage; private clearCursor; private clearCursors; private clearRemoteInputs; private clearTextInput; private clearTextInputs; /** Push a decoded frame into the surface's presenter, paint the very * first one synchronously, and schedule the next vsync tick. */ private enqueueFrame; /** Fold one arrival into the presenter's clock model and decide whether * this surface is streaming continuously enough to schedule off PTS. */ private trackArrival; /** Trim the offset window to ~{@link OFFSET_WINDOW_MS} of stream and map * PTS onto a bounded late-arrival quantile. * * The old fastest-path schedule added zero playout latency, but it also * exposed every reliable-stream ACK/GC stall as a frozen canvas followed * by a burst. A high quantile turns recurring jitter into a small steady * delay. The low quantile remains the baseline so the added delay is * observable and queue depth can be derived from it. */ private updateSchedule; private isLocalFastPath; /** Playout margin: how far behind the fastest observed path frames are * held so a late one still lands on its intended refresh. */ private playoutDelayMs; /** How many frames the presenter may hold while scheduling. * * A margin of `d` ms over a stream running at one frame every `i` ms * has `d / i` frames legitimately in hand at any moment. A fixed cap * would fight the margin exactly where it is needed most: at 240 Hz a * 50 ms margin spans 12 frames, so a cap of 4 would trim eight * not-yet-due frames per interval — dropping most of the stream in the * name of bounding it. * * There is no high-rate ceiling: the cap grows directly from the learned * positive interval. A non-positive or non-finite interval is not a * cadence and falls back to the initial refresh estimate. */ private smoothedQueueCap; private validFrameInterval; private schedulePresent; /** Track the display's refresh period from rAF deltas. Accepts every * positive cadence through {@link RAF_DELTA_MAX_MS} (10 Hz) and ignores * longer gaps as a stalled or backgrounded tick. * * Use the timestamp supplied by rAF, not `performance.now()`. Every rAF * callback in one browser frame receives the same timestamp, while the * wall clock advances as earlier surfaces draw. Measuring the latter * made a multi-pane frame's draw time look like a 1–3 ms display period * and corrupted the shared presentation clock. */ private noteRafInterval; /** vsync tick. * * Newest-wins until the surface proves it is streaming: that keeps * time-to-pixel minimal for the interactive case, where a repaint is a * response to input and any hold is felt as lag. * * Once streaming, each frame is drawn on the refresh its capture-time * PTS maps to. Frames not yet due stay queued — that is what makes a * 30 fps source hold each frame for exactly two refreshes on a 60 Hz * display instead of racing through the queue and then starving. */ private tickPresent; /** Present `queue[idx]`, closing everything older, and keep the rest. */ private presentIndex; /** Drain everything now, newest wins — for paths where rAF will not run * again soon (hidden tab) or the queue must not outlive the surface. */ private flushPresenter; /** Draw a frame to the backing canvas and notify listeners. Closes the * frame on the way out. */ private presentFrame; private discardPresenter; private discardAllPresenters; /** Present the newest queued frame (closing older ones) for every * surface, cancelling pending rAFs. Called when the tab goes hidden, * where the rAFs would otherwise never fire. * * Uses {@link flushPresenter}, not {@link tickPresent}: a scheduling * tick with nothing yet due re-arms rAF, and while hidden that callback * never runs — the queue would sit there holding decoder buffers until * the tab came back. */ private flushAllPresenters; /** * Create an off-DOM canvas for *surfaceId* if one does not already exist. * Existing canvases are never resized here — resizing clears content and * must only happen inside the decoder output callback where a new frame is * immediately drawn afterwards. */ private ensureCanvas; private webCodecsUnavailableWarned; /** Replace a decoder at a stream boundary. * * Chromium can accept an AV1 configure() that changes resolution, * profile, or level and then stop producing output. A new instance is * the reliable boundary. The old instance drains asynchronously, and * its output callback drops every frame once the map points elsewhere. */ private replaceDecoder; private initDecoder; private emitChange; } //# sourceMappingURL=SurfaceStore.d.ts.map