import type { ILiveBrowserVideoRendererStatistics, ILiveVideoAcceleration } from './plugins.js'; /** * Sampling period for the live browser statistics readout. One sample per * second keeps the readout legible and bounds application re-renders caused by * a busy view to one per period. */ export const browserStatisticsIntervalMs = 1_000; /** Values shown in the live browser view while a renderer is mounted. */ export interface IBrowserViewStatistics { /** Presented frame rate for video, decoded rate for canvas; undefined while measuring or stale. */ framesPerSecond: number | undefined; /** Frames retired without presentation since the renderer started. */ framesSkipped: number; /** Duration of the most recently completed input dispatch in milliseconds. */ inputRoundTripMs: number; /** Local input queue wait, separate from the operation reply. */ inputQueueMs?: number; /** Input operations awaiting a result at sampling time. */ inputCommandsInFlight: number; videoBitrateKbps?: number; videoRoundTripMs?: number; videoCodec?: string; videoEncoder?: string; videoBufferMs?: number; videoDecodeMs?: number; videoCaptureToDisplayMs?: number; videoFrameAgeMs?: number; } /** Renderer counters retained between two samples. */ export interface IBrowserStatisticsSample { framesDecoded: number; sampledAt: number; } export interface IBrowserStatisticsSampleResult { statistics: IBrowserViewStatistics; sample: IBrowserStatisticsSample; } /** * Derives the readout values from renderer counters. The frame rate is the * decoded-frame delta over the elapsed time since the previous sample; the * first sample and a zero-length period report 0 frames per second. */ export const sampleBrowserViewStatistics = ( statisticsArg: ILiveBrowserVideoRendererStatistics, previousArg: IBrowserStatisticsSample | undefined, nowArg: number, ): IBrowserStatisticsSampleResult => { const elapsedMs = previousArg ? nowArg - previousArg.sampledAt : 0; const framesPerSecond = previousArg && elapsedMs > 0 ? Math.max( 0, Math.round(((statisticsArg.framesDecoded - previousArg.framesDecoded) * 1_000) / elapsedMs), ) : 0; return { statistics: { framesPerSecond: statisticsArg.video ? (statisticsArg.video.sampledAt !== undefined && nowArg - statisticsArg.video.sampledAt <= 2500 && statisticsArg.video.presentedFramesPerSecond !== undefined ? Math.max(0, Math.round(statisticsArg.video.presentedFramesPerSecond)) : undefined) : framesPerSecond, framesSkipped: statisticsArg.framesSkipped, inputRoundTripMs: Math.max(0, Math.round(statisticsArg.lastInputRoundTripMs)), inputQueueMs: Math.max(0, Math.round(statisticsArg.lastInputQueueMs)), inputCommandsInFlight: statisticsArg.inputCommandsInFlight, ...(statisticsArg.video ? { videoBitrateKbps: Math.round(statisticsArg.video.bitrate / 1000), ...(statisticsArg.video.roundTripMs === undefined ? {} : { videoRoundTripMs: Math.round(statisticsArg.video.roundTripMs) }), videoCodec: statisticsArg.video.codec?.replace('video/', ''), videoEncoder: statisticsArg.video.sender?.encoderImplementation, ...(statisticsArg.video.jitterBufferMs === undefined ? {} : { videoBufferMs: Math.round(statisticsArg.video.jitterBufferMs) }), ...(statisticsArg.video.decodeMs === undefined ? {} : { videoDecodeMs: Math.round(statisticsArg.video.decodeMs) }), ...(statisticsArg.video.captureToDisplayMs === undefined ? {} : { videoCaptureToDisplayMs: Math.round(statisticsArg.video.captureToDisplayMs) }), ...(statisticsArg.video.frameAgeMs === undefined ? {} : { videoFrameAgeMs: Math.round(statisticsArg.video.frameAgeMs) }), } : {}), }, sample: { framesDecoded: statisticsArg.framesDecoded, sampledAt: nowArg }, }; }; export const browserViewStatisticsEqual = ( leftArg: IBrowserViewStatistics | undefined, rightArg: IBrowserViewStatistics | undefined, ): boolean => { if (leftArg === rightArg) return true; if (!leftArg || !rightArg) return false; return leftArg.framesPerSecond === rightArg.framesPerSecond && leftArg.framesSkipped === rightArg.framesSkipped && leftArg.inputRoundTripMs === rightArg.inputRoundTripMs && leftArg.inputQueueMs === rightArg.inputQueueMs && leftArg.inputCommandsInFlight === rightArg.inputCommandsInFlight && leftArg.videoBitrateKbps === rightArg.videoBitrateKbps && leftArg.videoRoundTripMs === rightArg.videoRoundTripMs && leftArg.videoCodec === rightArg.videoCodec && leftArg.videoEncoder === rightArg.videoEncoder && leftArg.videoBufferMs === rightArg.videoBufferMs && leftArg.videoDecodeMs === rightArg.videoDecodeMs && leftArg.videoCaptureToDisplayMs === rightArg.videoCaptureToDisplayMs && leftArg.videoFrameAgeMs === rightArg.videoFrameAgeMs; }; /** Formats the readout as one short line for the view overlay. */ export const formatBrowserViewStatistics = (statisticsArg: IBrowserViewStatistics): string => { const inFlight = statisticsArg.inputCommandsInFlight > 0 ? ` (${statisticsArg.inputCommandsInFlight} in flight)` : ''; const queue = statisticsArg.inputQueueMs ? ` · queue ${statisticsArg.inputQueueMs} ms` : ''; if (statisticsArg.videoBitrateKbps !== undefined) { const roundTrip = statisticsArg.videoRoundTripMs === undefined ? '' : ` · RTT ${statisticsArg.videoRoundTripMs} ms`; const delay = statisticsArg.videoCaptureToDisplayMs === undefined ? '' : ` · display ~${statisticsArg.videoCaptureToDisplayMs} ms`; const buffer = statisticsArg.videoBufferMs === undefined ? '' : ` · buffer ${statisticsArg.videoBufferMs} ms`; return `WebRTC · ${statisticsArg.framesPerSecond ?? '—'} fps · ${(statisticsArg.videoBitrateKbps / 1000).toFixed(1)} Mbps · ${statisticsArg.framesSkipped} dropped${delay}${buffer}${roundTrip} · reply ${statisticsArg.inputRoundTripMs} ms${queue}${inFlight}`; } return `${statisticsArg.framesPerSecond} fps · ${statisticsArg.framesSkipped} skipped · reply ${statisticsArg.inputRoundTripMs} ms${queue}${inFlight}`; }; /** One labelled reading in the screencast diagnostics panel. */ export interface IBrowserScreencastRow { label: string; value: string; } /** * Every screencast reading, in the order the panel lists them: the stream's identity first, then * its timing, then the input round trip, then what the host is rendering with. * * A reading the renderer has not reported is left out rather than shown as a zero, so the panel * only ever states what is measured. The one-line corner readout stays the glanceable summary; * this is the full set behind it. */ export const browserScreencastRows = ( statisticsArg: IBrowserViewStatistics | undefined, accelerationArg: ILiveVideoAcceleration | undefined, ): IBrowserScreencastRow[] => { const rows: IBrowserScreencastRow[] = []; const add = (labelArg: string, valueArg: string | undefined): void => { if (valueArg !== undefined) rows.push({ label: labelArg, value: valueArg }); }; const milliseconds = (valueArg: number | undefined, approximateArg = false): string | undefined => valueArg === undefined ? undefined : `${approximateArg ? '~' : ''}${valueArg} ms`; if (statisticsArg) { add('Codec', statisticsArg.videoCodec); add('Encoder', statisticsArg.videoEncoder); add('Frame rate', statisticsArg.framesPerSecond === undefined ? undefined : `${statisticsArg.framesPerSecond} fps`); add('Bitrate', statisticsArg.videoBitrateKbps === undefined ? undefined : `${(statisticsArg.videoBitrateKbps / 1000).toFixed(1)} Mbps`); add('Dropped frames', `${statisticsArg.framesSkipped}`); add('Decode', milliseconds(statisticsArg.videoDecodeMs)); add('Frame age', milliseconds(statisticsArg.videoFrameAgeMs, true)); add('Capture to display', milliseconds(statisticsArg.videoCaptureToDisplayMs, true)); add('Jitter buffer', milliseconds(statisticsArg.videoBufferMs)); add('Round trip', milliseconds(statisticsArg.videoRoundTripMs)); add('Input reply', milliseconds(statisticsArg.inputRoundTripMs)); add('Input queue', milliseconds(statisticsArg.inputQueueMs)); add('Commands in flight', `${statisticsArg.inputCommandsInFlight}`); } if (accelerationArg) { add('GPU compositing', accelerationArg.compositing); add('Video encoding', accelerationArg.videoEncoding); } return rows; };