feat(quality): implement strict v13 quality shadow (#291)

This commit is contained in:
AAA authored and GitHub committed 2026-08-27 02:57:38 +08:00
1 parent 5a081fb119
commit 501df27bce
22 files changed
+1467 -203

No files matched your search

+47 -20
View File
@@ -116,6 +116,7 @@ export type ViewerPresentationAction =
}
| { type: "media-bound"; generation: number; revision: number }
| { type: "frame-presented"; generation: number; revision: number }
| { type: "frame-proof-reset"; generation: number; revision: number }
| { type: "autoplay-blocked"; generation: number; revision: number }
| { type: "autoplay-cleared"; generation: number }
| { type: "playback-failed"; generation: number; revision: number }
@@ -210,8 +211,7 @@ export function reduceViewerPresentation(
kind: action.kind,
},
routeStatus:
state.routeStatus?.revision === action.revision &&
state.routeStatus.state === "waiting"
state.routeStatus?.revision === action.revision
? state.routeStatus
: null,
media: preserveMedia
@@ -288,7 +288,11 @@ export function reduceViewerPresentation(
case "media-bound": {
if (
(state.revision !== null && action.revision < state.revision) ||
(state.media && action.generation <= state.media.generation)
(state.media && action.generation <= state.media.generation) ||
(state.routeStatus?.revision === action.revision &&
state.routeStatus.state === "failed") ||
(state.revision === action.revision &&
state.failure === "ROUTE_EXHAUSTED")
) {
return state;
}
@@ -315,7 +319,9 @@ export function reduceViewerPresentation(
!state.media ||
state.media.generation !== action.generation ||
state.media.revision !== action.revision ||
state.revision !== action.revision
state.revision !== action.revision ||
state.routeStatus?.state === "failed" ||
state.failure === "ROUTE_EXHAUSTED"
) {
return state;
}
@@ -326,10 +332,22 @@ export function reduceViewerPresentation(
retainedFrame: false,
routeStatus: null,
failure:
state.failure === "PLAYBACK_FAILED" ||
state.failure === "ROUTE_EXHAUSTED"
? null
: state.failure,
state.failure === "PLAYBACK_FAILED" ? null : state.failure,
};
case "frame-proof-reset":
if (
!state.media ||
state.media.generation !== action.generation ||
state.media.revision !== action.revision ||
state.revision !== action.revision ||
!state.media.framePresented
) {
return state;
}
return {
...state,
media: { ...state.media, framePresented: false },
retainedFrame: true,
};
case "autoplay-blocked":
if (
@@ -540,6 +558,15 @@ export function deriveViewerPresentation(
};
}
if (state.routeStatus?.state === "failed") {
return presentation(
"route-failed",
"没有可用的媒体线路",
frameOverlay,
state,
);
}
switch (state.failure) {
case "ROUTE_EXHAUSTED":
return presentation(
@@ -607,6 +634,18 @@ export function deriveViewerPresentation(
state,
);
}
if (
state.signal === "reconnecting" ||
state.connection === "reconnecting" ||
state.connection === "failed"
) {
return presentation(
"recovering",
"正在恢复连接",
frameOverlay,
state,
);
}
if (state.media && state.media.revision === state.revision) {
return presentation(
"receiving",
@@ -623,18 +662,6 @@ export function deriveViewerPresentation(
state,
);
}
if (
state.signal === "reconnecting" ||
state.connection === "reconnecting" ||
state.connection === "failed"
) {
return presentation(
"recovering",
"正在恢复连接",
frameOverlay,
state,
);
}
if (state.host === "stopped" || state.host === "unknown") {
return presentation(
"waiting-host",
+75 -27
View File
@@ -96,10 +96,6 @@ export function qualityEvidenceWindowFromMetrics(
width !== null && height !== null
? { width, height }
: { width: null, height: null };
const freezeDuration = boundedNumber(
metrics.intervalFreezeDurationMs,
windowMs,
);
const candidate: ViewerQualityEvidenceMetrics = {
...dimensions,
framesPerSecond: boundedNumber(metrics.framesPerSecond, 240),
@@ -120,8 +116,22 @@ export function qualityEvidenceWindowFromMetrics(
10_000,
),
decodeMsPerFrame: boundedNumber(metrics.intervalDecodeMs, 60_000),
freezeCountDelta: boundedInteger(metrics.intervalFreezeCount, 10_000),
freezeDurationMsDelta: freezeDuration,
freezeCountDelta: boundedInteger(
metrics.intervalFreezeCount,
Number.MAX_SAFE_INTEGER,
),
freezeDurationMsDelta: boundedNumber(
metrics.intervalFreezeDurationMs,
Number.MAX_SAFE_INTEGER,
),
pauseCountDelta: boundedInteger(
metrics.intervalPauseCount,
Number.MAX_SAFE_INTEGER,
),
pauseDurationMsDelta: boundedNumber(
metrics.intervalPauseDurationMs,
Number.MAX_SAFE_INTEGER,
),
codec: boundedString(
metrics.codec,
64,
@@ -175,7 +185,9 @@ export function qualityEvidenceWindowFromMetrics(
export class ViewerQualityEvidenceReporter {
private connectionId: string | null = null;
private routeRevision: number | null = null;
private presentationEpoch = 0;
private presentationEligible = false;
private baselinePending = true;
private sequence = 0;
private lastSentAtMs: number | null = null;
private lastSampleTimestampMs: number | null = null;
@@ -185,11 +197,16 @@ export class ViewerQualityEvidenceReporter {
private readonly now: () => number = Date.now,
) {}
offer(snapshot: PeerSnapshot, routeRevision: number): boolean {
offer(
snapshot: PeerSnapshot,
routeRevision: number,
presentationEligible: boolean,
): boolean {
return this.offerMetrics(
snapshot.connectionId,
snapshot.metrics,
routeRevision,
presentationEligible,
);
}
@@ -197,6 +214,7 @@ export class ViewerQualityEvidenceReporter {
connectionId: string,
metrics: ConnectionMetrics,
routeRevision: number,
presentationEligible: boolean,
): boolean {
if (
!Number.isSafeInteger(routeRevision) ||
@@ -205,8 +223,14 @@ export class ViewerQualityEvidenceReporter {
) {
return false;
}
const window = qualityEvidenceWindowFromMetrics(metrics);
const sampleTimestampMs = metrics.sampleTimestampMs;
if (this.connectionId !== connectionId) {
this.connectionId = connectionId;
this.sequence = 0;
this.lastSampleTimestampMs = null;
this.baselinePending = true;
}
const window = qualityEvidenceWindowFromMetrics(metrics);
if (
!window ||
sampleTimestampMs === null ||
@@ -215,27 +239,36 @@ export class ViewerQualityEvidenceReporter {
) {
return false;
}
if (this.connectionId !== connectionId) {
this.connectionId = connectionId;
this.routeRevision = routeRevision;
this.sequence = 0;
this.lastSentAtMs = null;
this.lastSampleTimestampMs = null;
} else if (this.routeRevision !== routeRevision) {
this.routeRevision = routeRevision;
this.lastSentAtMs = null;
this.lastSampleTimestampMs = null;
}
if (this.sequence > Number.MAX_SAFE_INTEGER) {
return false;
}
if (
this.lastSampleTimestampMs !== null &&
sampleTimestampMs <= this.lastSampleTimestampMs
) {
return false;
}
this.lastSampleTimestampMs = sampleTimestampMs;
if (!presentationEligible) {
if (this.presentationEligible) {
this.invalidatePresentation();
}
return false;
}
if (!this.presentationEligible) {
this.presentationEligible = true;
this.baselinePending = true;
}
const decodedProgress =
metrics.intervalFramesDecoded !== null &&
metrics.intervalFramesDecoded > 0;
if (!decodedProgress) {
return false;
}
if (this.baselinePending) {
this.baselinePending = false;
return false;
}
if (this.sequence > Number.MAX_SAFE_INTEGER) {
return false;
}
const now = this.now();
if (
!Number.isFinite(now) ||
@@ -250,6 +283,7 @@ export class ViewerQualityEvidenceReporter {
guard: {
connectionId,
routeRevision,
presentationEpoch: this.presentationEpoch,
},
sequence: this.sequence,
...window,
@@ -266,13 +300,26 @@ export class ViewerQualityEvidenceReporter {
this.sequence += 1;
this.lastSentAtMs = now;
this.lastSampleTimestampMs = sampleTimestampMs;
return true;
}
invalidatePresentation(): void {
if (!this.presentationEligible) {
return;
}
if (this.presentationEpoch < Number.MAX_SAFE_INTEGER) {
this.presentationEpoch += 1;
}
this.presentationEligible = false;
this.baselinePending = true;
this.sequence = 0;
}
reset(): void {
this.connectionId = null;
this.routeRevision = null;
this.presentationEpoch = 0;
this.presentationEligible = false;
this.baselinePending = true;
this.sequence = 0;
this.lastSentAtMs = null;
this.lastSampleTimestampMs = null;
@@ -286,7 +333,8 @@ function sameViewerQualityEvidenceIdentity(
return (
previous.viewerPeerId === next.viewerPeerId &&
qualityEvidenceUpstreamMatches(previous, next.upstream) &&
previous.guard.connectionId === next.guard.connectionId
previous.guard.connectionId === next.guard.connectionId &&
previous.guard.presentationEpoch === next.guard.presentationEpoch
);
}
+152 -15
View File
@@ -63,6 +63,7 @@ import {
deriveViewerPresentation,
reduceViewerPresentation,
viewerFailureFromServerCode,
type ViewerPresentationAction,
type ViewerRouteKind,
type ViewerStage,
} from "../media/viewer-presentation";
@@ -155,7 +156,7 @@ function StageOverlayIcon({ stage }: { stage: ViewerStage }) {
}
export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
const [presentationState, dispatchPresentation] = useReducer(
const [presentationState, dispatchPresentationState] = useReducer(
reduceViewerPresentation,
INITIAL_VIEWER_PRESENTATION_STATE,
);
@@ -234,6 +235,18 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
const peerRef = useRef<ViewerPeer | null>(null);
const viewerSfuRouteRef = useRef<ViewerSfuRoute | null>(null);
const signalRef = useRef<SignalingClient | null>(null);
const presentationStateRef = useRef(presentationState);
presentationStateRef.current = presentationState;
function dispatchPresentation(action: ViewerPresentationAction): void {
presentationStateRef.current = reduceViewerPresentation(
presentationStateRef.current,
action,
);
dispatchPresentationState(action);
}
const qualityEvidenceReporterRef =
useRef<ViewerQualityEvidenceReporter | null>(null);
const qualityFrameProofGenerationRef = useRef<number | null>(null);
const displayNameRef = useRef(displayName);
const remoteMediaRef = useRef<RemoteMediaBinding | null>(null);
const mediaGenerationRef = useRef(0);
@@ -279,6 +292,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
videoTrackKey,
generation: ++mediaGenerationRef.current,
};
invalidateQualityPresentation();
remoteMediaRef.current = next;
setRemoteMedia(next);
dispatchPresentation({
@@ -289,22 +303,62 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
}
function clearRemoteMedia(): void {
invalidateQualityPresentation();
remoteMediaRef.current = null;
setRemoteMedia(null);
dispatchPresentation({ type: "media-cleared" });
}
function resetCurrentFrameProof(): void {
const binding = remoteMediaRef.current;
if (!binding) {
return;
}
dispatchPresentation({
type: "frame-proof-reset",
generation: binding.generation,
revision: binding.revision,
});
}
function invalidateQualityPresentation(): void {
qualityEvidenceReporterRef.current?.invalidatePresentation();
qualityFrameProofGenerationRef.current = null;
}
function invalidatePresentedMedia(): void {
invalidateQualityPresentation();
resetCurrentFrameProof();
}
function mediaBindingIsCurrent(binding: RemoteMediaBinding): boolean {
const current = remoteMediaRef.current;
return (
current?.generation === binding.generation &&
current.revision === binding.revision &&
current.stream === binding.stream
);
}
function attemptPlayback(
video: HTMLVideoElement,
binding: RemoteMediaBinding,
): void {
void video.play().then(
() =>
() => {
if (!mediaBindingIsCurrent(binding)) {
return;
}
dispatchPresentation({
type: "autoplay-cleared",
generation: binding.generation,
}),
});
},
(error: unknown) => {
if (!mediaBindingIsCurrent(binding)) {
return;
}
invalidatePresentedMedia();
if (isAutoplayPolicyRejection(error)) {
dispatchPresentation({
type: "autoplay-blocked",
@@ -384,16 +438,19 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
let relayChildEvidenceCurrent: ViewerQualityEvidencePresentation | null =
null;
let relayChildEvidenceTimer: number | null = null;
let sfuTransportConnected = false;
let pageSuspended = document.visibilityState !== "visible";
const syncDecodedFrameStallPause = (): void => {
decodedFrameStall.setPaused(currentHostPaused || pageSuspended);
};
const suspendForPageLifecycle = (): void => {
invalidatePresentedMedia();
pageSuspended = true;
syncDecodedFrameStallPause();
};
const recoverFromPageLifecycle = (): void => {
invalidatePresentedMedia();
pageSuspended = document.visibilityState !== "visible";
syncDecodedFrameStallPause();
if (pageSuspended) return;
@@ -485,6 +542,50 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
const qualityEvidenceReporter = new ViewerQualityEvidenceReporter(
(message) => active && signal.send(message),
);
qualityEvidenceReporterRef.current = qualityEvidenceReporter;
function qualityPresentationEligible(
connectionConnected: boolean,
): boolean {
const state = presentationStateRef.current;
const binding = remoteMediaRef.current;
const video = videoRef.current;
return Boolean(
connectionConnected &&
!pageSuspended &&
document.visibilityState === "visible" &&
!currentHostPaused &&
currentRouteAssignment !== null &&
currentRouteAssignment.upstream.kind !== "none" &&
currentRouteConnectionId !== null &&
binding &&
binding.revision === currentRouteRevision &&
qualityFrameProofGenerationRef.current === binding.generation &&
state.routeStatus?.state !== "failed" &&
state.failure !== "ROUTE_EXHAUSTED" &&
video &&
!video.paused &&
!video.ended,
);
}
function offerPeerQualityEvidence(
snapshot: PeerSnapshot,
peer: ViewerPeer,
): void {
const connectionHealthy =
snapshot.connectionState === "connected" && !peer.isRecovering();
const qualityEligible =
qualityPresentationEligible(connectionHealthy);
if (!connectionHealthy) {
invalidatePresentedMedia();
} else if (!qualityEligible) {
invalidateQualityPresentation();
}
qualityEvidenceReporter.offer(
snapshot,
currentRouteRevision,
qualityEligible,
);
}
function activateRouteIdentity(
revision: number,
assignment: ParticipantRouteAssignment,
@@ -799,6 +900,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
return;
}
currentAssignment = { parentPeerId: null, childPeerIds: [] };
sfuTransportConnected = false;
setSfuUpstream(null);
clearPeerState();
viewerRelay?.setChildren([]);
@@ -838,6 +940,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
currentRouteConnectionId,
metrics,
revision,
qualityPresentationEligible(sfuTransportConnected),
);
}
}
@@ -848,6 +951,13 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
},
onSfuState: (state, revision) => {
if (active && viewerSfuRoute === route) {
const connected = state === "connected";
if (sfuTransportConnected !== connected) {
sfuTransportConnected = connected;
if (!connected) {
invalidatePresentedMedia();
}
}
setSfuUpstream((current) =>
current ? { ...current, connectionState: state } : null,
);
@@ -862,6 +972,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
if (!active || viewerSfuRoute !== route || available) {
return;
}
invalidatePresentedMedia();
setSfuUpstream((current) =>
current ? { ...current, connectionState: "reconnecting" } : current,
);
@@ -889,6 +1000,9 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
const relay = ensureViewerRelay();
relay?.setStream(nextStream);
bindRemoteStream(nextStream, revision);
if (initialVideoStream) {
sfuTransportConnected = true;
}
setSfuUpstream(
(current) =>
current ?? { connectionState: "connected", metrics: null },
@@ -913,6 +1027,8 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
const route = viewerSfuRoute;
discardPendingPeer();
viewerSfuRoute = null;
sfuTransportConnected = false;
invalidatePresentedMedia();
if (viewerSfuRouteRef.current === route) {
viewerSfuRouteRef.current = null;
}
@@ -1018,7 +1134,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
snapshot.metrics.intervalFramesDecoded,
snapshot.connectionId,
);
qualityEvidenceReporter.offer(snapshot, currentRouteRevision);
offerPeerQualityEvidence(snapshot, peer);
setPeerSnapshot(snapshot);
dispatchPresentation({
type: "connection",
@@ -1104,7 +1220,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
snapshot.metrics.intervalFramesDecoded,
snapshot.connectionId,
);
qualityEvidenceReporter.offer(snapshot, currentRouteRevision);
offerPeerQualityEvidence(snapshot, peer);
if (
!currentHostOnline &&
(snapshot.connectionState === "failed" ||
@@ -1131,12 +1247,12 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
if (viewerSfuRoute) {
return viewerSfuRoute.reportPeerFailure(parentPeerId, connectionId);
}
invalidateQualityPresentation();
dispatchPresentation({
type: "failure",
failure: "ROUTE_EXHAUSTED",
revision: currentRouteRevision,
});
setFrameProofEpoch((current) => current + 1);
return true;
},
},
@@ -1209,6 +1325,9 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
currentHostOnline = message.hostOnline;
setHostOnline(message.hostOnline);
const sharingPaused = message.hostPaused ?? false;
if (currentHostPaused !== sharingPaused && sharingPaused) {
invalidatePresentedMedia();
}
currentHostPaused = sharingPaused;
syncDecodedFrameStallPause();
dispatchPresentation({
@@ -1342,14 +1461,14 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
return;
}
if (message.type === "route-status") {
if (message.state === "failed") {
invalidateQualityPresentation();
}
dispatchPresentation({
type: "route-status",
revision: message.revision,
state: message.state,
});
if (message.state === "failed") {
setFrameProofEpoch((current) => current + 1);
}
return;
}
if (message.type === "viewer-quality-evidence") {
@@ -1452,6 +1571,9 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
}
if (message.type === "host-status") {
currentHostOnline = message.online;
if (currentHostPaused !== message.paused && message.paused) {
invalidatePresentedMedia();
}
currentHostPaused = message.paused;
syncDecodedFrameStallPause();
setHostOnline(message.online);
@@ -1471,6 +1593,7 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
return;
}
if (message.type === "sharing-stopped") {
invalidatePresentedMedia();
currentRouteAssignment = null;
currentRouteConnectionId = null;
pendingRouteConnection = null;
@@ -1586,6 +1709,9 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
window.removeEventListener("pageshow", recoverFromPageLifecycle);
currentPeerId = null;
qualityEvidenceReporter.reset();
if (qualityEvidenceReporterRef.current === qualityEvidenceReporter) {
qualityEvidenceReporterRef.current = null;
}
clearRelayChildEvidence();
sfuStandbyPrewarmer?.dispose();
signal.stop();
@@ -1656,13 +1782,18 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
if (!video || !remoteMedia) {
return;
}
return observeCompositedVideoFrame(video, remoteMedia.stream, () =>
const binding = remoteMedia;
return observeCompositedVideoFrame(video, binding.stream, () => {
if (!mediaBindingIsCurrent(binding)) {
return;
}
qualityFrameProofGenerationRef.current = binding.generation;
dispatchPresentation({
type: "frame-presented",
generation: remoteMedia.generation,
revision: remoteMedia.revision,
}),
);
generation: binding.generation,
revision: binding.revision,
});
});
}, [frameProofEpoch, presentationState.connection, remoteMedia]);
function retryConnection(): void {
@@ -1921,10 +2052,14 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
autoPlay
controls={
presentation.overlay === "none" ||
presentation.stage === "needs-play"
presentation.stage === "needs-play" ||
(presentation.stage === "receiving" &&
presentation.hasRetainedFrame)
}
playsInline
onPlay={() => {
invalidateQualityPresentation();
setFrameProofEpoch((current) => current + 1);
const binding = remoteMediaRef.current;
if (binding) {
dispatchPresentation({
@@ -1933,6 +2068,8 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) {
});
}
}}
onPause={invalidateQualityPresentation}
onEnded={invalidateQualityPresentation}
/>
{presentation.overlay === "blocking" && (
<div className="stage-placeholder" role="status">
+4
View File
@@ -45,6 +45,8 @@ export interface ConnectionMetrics {
intervalFramesDropped: number | null;
intervalFreezeCount: number | null;
intervalFreezeDurationMs: number | null;
intervalPauseCount: number | null;
intervalPauseDurationMs: number | null;
intervalRetransmittedPackets: number | null;
intervalRetransmittedBytes: number | null;
codec: string | null;
@@ -125,6 +127,8 @@ export const EMPTY_METRICS: ConnectionMetrics = {
intervalFramesDropped: null,
intervalFreezeCount: null,
intervalFreezeDurationMs: null,
intervalPauseCount: null,
intervalPauseDurationMs: null,
intervalRetransmittedPackets: null,
intervalRetransmittedBytes: null,
codec: null,
+27
View File
@@ -26,6 +26,8 @@ export interface StatsAccumulator {
previousFramesDropped: number | null;
previousFreezeCount: number | null;
previousTotalFreezesDuration: number | null;
previousPauseCount: number | null;
previousTotalPausesDuration: number | null;
previousRetransmittedPackets: number | null;
previousRetransmittedBytes: number | null;
previousVideoJitterBufferDelay: number | null;
@@ -92,6 +94,8 @@ export function createStatsAccumulator(): StatsAccumulator {
previousFramesDropped: null,
previousFreezeCount: null,
previousTotalFreezesDuration: null,
previousPauseCount: null,
previousTotalPausesDuration: null,
previousRetransmittedPackets: null,
previousRetransmittedBytes: null,
previousVideoJitterBufferDelay: null,
@@ -493,6 +497,8 @@ export function collectConnectionMetricsFromReport(
const framesDropped = numberValue(media, "framesDropped");
const freezeCount = numberValue(media, "freezeCount");
const totalFreezesDuration = numberValue(media, "totalFreezesDuration");
const pauseCount = numberValue(media, "pauseCount");
const totalPausesDuration = numberValue(media, "totalPausesDuration");
const retransmittedPackets = numberValue(
media,
direction === "send"
@@ -609,6 +615,22 @@ export function collectConnectionMetricsFromReport(
previous.previousTotalFreezesDuration,
sampleWindowMs !== null,
);
const intervalPauseCount =
direction === "receive"
? intervalDelta(
pauseCount,
previous.previousPauseCount,
sampleWindowMs !== null,
)
: null;
const pauseDurationDelta =
direction === "receive"
? intervalDelta(
totalPausesDuration,
previous.previousTotalPausesDuration,
sampleWindowMs !== null,
)
: null;
const intervalRetransmittedPackets = intervalDelta(
retransmittedPackets,
previous.previousRetransmittedPackets,
@@ -643,6 +665,8 @@ export function collectConnectionMetricsFromReport(
previous.previousFramesDropped = framesDropped;
previous.previousFreezeCount = freezeCount;
previous.previousTotalFreezesDuration = totalFreezesDuration;
previous.previousPauseCount = pauseCount;
previous.previousTotalPausesDuration = totalPausesDuration;
previous.previousRetransmittedPackets = retransmittedPackets;
previous.previousRetransmittedBytes = retransmittedBytes;
previous.previousVideoJitterBufferDelay = videoJitterBufferDelay;
@@ -840,6 +864,9 @@ export function collectConnectionMetricsFromReport(
intervalFreezeCount,
intervalFreezeDurationMs:
freezeDurationDelta === null ? null : freezeDurationDelta * 1_000,
intervalPauseCount,
intervalPauseDurationMs:
pauseDurationDelta === null ? null : pauseDurationDelta * 1_000,
intervalRetransmittedPackets,
intervalRetransmittedBytes,
codec: codecEvidence.codec,
+12
View File
@@ -19,6 +19,8 @@ import {
type CandidateReservation,
type CandidateTuple,
type OperationSnapshot,
type RouteQualityEvidenceInput,
type RouteQualityEvidenceResult,
type RouteSnapshot,
} from "./room-route-controller.js";
import type { RoomStore } from "./room-store.js";
@@ -263,6 +265,16 @@ export class HybridMediaRouter {
);
}
observeQualityEvidence(
input: RouteQualityEvidenceInput & { roomId: string },
): RouteQualityEvidenceResult {
const { roomId, ...evidence } = input;
return (
this.rooms.get(roomId)?.controller?.observeQualityEvidence(evidence) ??
"rejected"
);
}
isActivePeerParentOf(
roomId: string,
parentPeerId: string,
+281
View File
@@ -3,10 +3,12 @@ import { debuglog } from "node:util";
import {
MAX_MEDIA_ROUTE_REVISION,
VIEWER_QUALITY_EVIDENCE_EXPIRY_MS,
type RouteDemandReason,
type RouteDiagnosticFinalRoute,
type RouteDiagnosticRejectionBucket,
type RouteDiagnosticSnapshot,
type ViewerQualityEvidenceMetrics,
} from "../shared/protocol.js";
import { assertEndpointMediaCopyCapacity } from "../shared/media-copy-accounting.js";
@@ -221,6 +223,46 @@ interface RouteTimingRecord {
rejectionBucket: RouteDiagnosticRejectionBucket;
}
interface RouteQualityObservation {
childSessionId: string;
upstreamKind: "peer" | "sfu";
upstreamPeerId: string | null;
connectionId: string;
presentationEpoch: number;
eligibleWindows: number;
eligibleDurationMs: number;
freezeWindows: number;
freezeCount: number;
freezeDurationMs: number;
pauseCount: number;
pauseDurationMs: number;
lastAcceptedAtMs: number;
}
export interface RouteQualityEvidenceInput {
childPeerId: string;
childSessionId: string;
routeRevision: number;
connectionId: string;
upstream: { kind: "peer"; peerId: string } | { kind: "sfu" };
presentationEpoch: number;
windowMs: number;
metrics: Pick<
ViewerQualityEvidenceMetrics,
| "framesDecodedDelta"
| "freezeCountDelta"
| "freezeDurationMsDelta"
| "pauseCountDelta"
| "pauseDurationMsDelta"
>;
acceptedAtMs: number;
}
export type RouteQualityEvidenceResult =
| "rejected"
| "accepted"
| "observed";
export class RoomRouteController<Resource = unknown> {
private readonly participants = new Map<string, Participant>();
private readonly upstreamByViewer = new Map<string, CommittedEdge<Resource>>();
@@ -232,6 +274,11 @@ export class RoomRouteController<Resource = unknown> {
private nextJoinOrder = 0;
private paused = false;
private readonly routeTimings = new Map<string, RouteTimingRecord>();
private readonly qualityObservations = new Map<
string,
RouteQualityObservation
>();
private readonly qualityBaselinesPending = new Set<string>();
private readonly directContinuations = new Map<string, DirectContinuation>();
private sfuBootstrapIntent?: SfuBootstrapIntent;
private sfuBootstrapExhaustedAtFactVersion?: number;
@@ -311,6 +358,7 @@ export class RoomRouteController<Resource = unknown> {
: "waiting"
: finalRoute,
rejectionBucket: record?.rejectionBucket ?? "none",
quality: this.diagnosticQuality(viewer.peerId, edge, nowMs),
};
},
);
@@ -348,6 +396,7 @@ export class RoomRouteController<Resource = unknown> {
let changed = false;
const previousSessionId = current.sessionId;
if (previousSessionId !== input.sessionId) {
this.clearQualityForParticipant(input.peerId);
const revisionBefore = this.revision;
if (this.operationUsesParticipantSession(input.peerId)) {
released.push(...this.abortOperation(nowMs, "stale"));
@@ -413,6 +462,7 @@ export class RoomRouteController<Resource = unknown> {
const participant = this.participants.get(peerId);
if (!participant || participant.sessionId !== sessionId) return false;
participant.sessionId = null;
this.clearQualityForParticipant(peerId);
this.directContinuations.delete(peerId);
this.clearSfuBootstrapForDemand(peerId);
this.touchFacts();
@@ -429,6 +479,7 @@ export class RoomRouteController<Resource = unknown> {
if (participant.departureConfirmed && participant.sessionId === null &&
participant.effectiveDownstreamCapacity === 0) return true;
participant.sessionId = null;
this.clearQualityForParticipant(peerId);
participant.departureConfirmed = true;
participant.effectiveDownstreamCapacity = 0;
this.directContinuations.delete(peerId);
@@ -521,6 +572,7 @@ export class RoomRouteController<Resource = unknown> {
return false;
}
if (edge.usable) {
this.clearQualityForParticipant(guard.childPeerId);
edge.usable = false;
this.directContinuations.delete(guard.childPeerId);
child.blockedAtFactVersion = undefined;
@@ -548,6 +600,7 @@ export class RoomRouteController<Resource = unknown> {
publication.generation !== guard.generation ||
publication.connectionId !== guard.connectionId) return false;
if (publication.usable) {
this.clearSfuQuality();
publication.usable = false;
this.directContinuations.clear();
this.sfuBootstrapIntent = undefined;
@@ -582,6 +635,7 @@ export class RoomRouteController<Resource = unknown> {
return false;
}
if (input.newConnectionId === edge.connectionId) return true;
this.clearQualityForParticipant(input.childPeerId);
edge.connectionId = input.newConnectionId;
const child = this.participants.get(input.childPeerId);
if (child) {
@@ -603,6 +657,7 @@ export class RoomRouteController<Resource = unknown> {
}
edge.physicalActive = false;
edge.usable = false;
this.clearQualityForParticipant(guard.childPeerId);
child.blockedAtFactVersion = undefined;
this.revision = this.allocateRevision();
if (this.operation) this.operation.baseRevision = this.revision;
@@ -624,6 +679,7 @@ export class RoomRouteController<Resource = unknown> {
publication.connectionId !== guard.connectionId) return [];
publication.physicalActive = false;
publication.usable = false;
this.clearSfuQuality();
this.revision = this.allocateRevision();
if (this.operation) this.operation.baseRevision = this.revision;
this.touchFacts();
@@ -633,10 +689,126 @@ export class RoomRouteController<Resource = unknown> {
setPaused(paused: boolean, nowMs?: number): readonly Resource[] {
if (this.paused === paused) return [];
this.paused = paused;
if (paused) {
this.qualityObservations.clear();
this.qualityBaselinesPending.clear();
}
this.touchFacts();
return paused ? this.abortOperation(nowMs, "aborted") : [];
}
observeQualityEvidence(
input: RouteQualityEvidenceInput,
): RouteQualityEvidenceResult {
if (
this.paused ||
input.routeRevision !== this.revision ||
!Number.isSafeInteger(input.presentationEpoch) ||
input.presentationEpoch < 0 ||
!Number.isSafeInteger(input.windowMs) ||
input.windowMs <= 0 ||
!Number.isSafeInteger(input.acceptedAtMs) ||
input.acceptedAtMs < 0
) {
return "rejected";
}
const child = this.participants.get(input.childPeerId);
const edge = this.upstreamByViewer.get(input.childPeerId);
if (
!child ||
child.sessionId !== input.childSessionId ||
!edge ||
!edge.usable ||
!edge.physicalActive ||
edge.childSessionId !== input.childSessionId ||
edge.connectionId !== input.connectionId ||
!this.qualityUpstreamMatches(edge, input.upstream) ||
!this.sourceUsableForQuality(edge)
) {
return "rejected";
}
const upstreamPeerId =
input.upstream.kind === "peer" ? input.upstream.peerId : null;
let observation = this.qualityObservations.get(input.childPeerId);
const sameEdgeObservation =
observation?.childSessionId === input.childSessionId &&
observation.upstreamKind === input.upstream.kind &&
observation.upstreamPeerId === upstreamPeerId &&
observation.connectionId === input.connectionId;
if (
sameEdgeObservation &&
observation &&
input.presentationEpoch < observation.presentationEpoch
) {
return "rejected";
}
if (
!sameEdgeObservation ||
!observation ||
observation.presentationEpoch !== input.presentationEpoch
) {
observation = {
childSessionId: input.childSessionId,
upstreamKind: input.upstream.kind,
upstreamPeerId,
connectionId: input.connectionId,
presentationEpoch: input.presentationEpoch,
eligibleWindows: 0,
eligibleDurationMs: 0,
freezeWindows: 0,
freezeCount: 0,
freezeDurationMs: 0,
pauseCount: 0,
pauseDurationMs: 0,
lastAcceptedAtMs: input.acceptedAtMs,
};
this.qualityObservations.set(input.childPeerId, observation);
}
const metrics = input.metrics;
if (
metrics.framesDecodedDelta === null ||
metrics.framesDecodedDelta <= 0 ||
metrics.freezeCountDelta === null ||
metrics.freezeDurationMsDelta === null ||
metrics.pauseCountDelta === null ||
metrics.pauseDurationMsDelta === null
) {
return "accepted";
}
observation.lastAcceptedAtMs = input.acceptedAtMs;
if (this.qualityBaselinesPending.delete(input.childPeerId)) {
return "accepted";
}
observation.eligibleWindows = safeAdd(observation.eligibleWindows, 1);
observation.eligibleDurationMs = safeAdd(
observation.eligibleDurationMs,
input.windowMs,
);
if (metrics.freezeCountDelta > 0) {
observation.freezeWindows = safeAdd(observation.freezeWindows, 1);
}
observation.freezeCount = safeAdd(
observation.freezeCount,
metrics.freezeCountDelta,
);
observation.freezeDurationMs = safeAdd(
observation.freezeDurationMs,
metrics.freezeDurationMsDelta,
);
observation.pauseCount = safeAdd(
observation.pauseCount,
metrics.pauseCountDelta,
);
observation.pauseDurationMs = safeAdd(
observation.pauseDurationMs,
metrics.pauseDurationMsDelta,
);
return "observed";
}
reconcile(nowMs: number): ReconcileResult<Resource> {
const released: Resource[] = [];
const validation = this.validateOrAdvance(nowMs);
@@ -1164,6 +1336,8 @@ export class RoomRouteController<Resource = unknown> {
for (const resource of this.committedResources()) resources.add(resource);
this.operation = undefined;
this.upstreamByViewer.clear();
this.qualityObservations.clear();
this.qualityBaselinesPending.clear();
this.hostPublication = null;
this.participants.clear();
this.routeTimings.clear();
@@ -1373,6 +1547,7 @@ export class RoomRouteController<Resource = unknown> {
private commitAttempt(operation: ChildOperation<Resource>, attempt: Attempt<Resource>): Resource[] {
const beforeResources = new Set(this.committedResources());
const old = this.upstreamByViewer.get(operation.childPeerId);
this.clearQualityForParticipant(operation.childPeerId);
if (attempt.tuple.kind === "sfu" && attempt.tuple.publication === "replace") {
this.removePublicationGeneration(this.hostPublication!.generation);
@@ -2017,6 +2192,7 @@ export class RoomRouteController<Resource = unknown> {
const edge = this.upstreamByViewer.get(participant.peerId);
if (edge && edge.transport !== "direct" && edge.physicalActive) released.push(edge.resource);
this.upstreamByViewer.delete(participant.peerId);
this.clearQualityForParticipant(participant.peerId);
this.participants.delete(participant.peerId);
this.routeTimings.delete(participant.peerId);
this.directContinuations.delete(participant.peerId);
@@ -2027,6 +2203,7 @@ export class RoomRouteController<Resource = unknown> {
}
if (removed.length > 0) {
if (!this.hasSfuSubscribers() && this.hostPublication) {
this.clearSfuQuality();
if (this.hostPublication.physicalActive) released.push(this.hostPublication.resource);
this.hostPublication = null;
}
@@ -2408,6 +2585,99 @@ export class RoomRouteController<Resource = unknown> {
: { kind: "viewer", ordinal };
}
private diagnosticQuality(
childPeerId: string,
edge: CommittedEdge<Resource> | undefined,
nowMs: number,
): RouteDiagnosticSnapshot["children"][number]["quality"] {
const observation = this.qualityObservations.get(childPeerId);
const participant = this.participants.get(childPeerId);
if (
this.paused ||
!observation ||
observation.eligibleWindows === 0 ||
nowMs >=
observation.lastAcceptedAtMs + VIEWER_QUALITY_EVIDENCE_EXPIRY_MS ||
!participant?.sessionId ||
participant.sessionId !== observation.childSessionId ||
!edge ||
!edge.usable ||
!edge.physicalActive ||
edge.connectionId !== observation.connectionId ||
!this.qualityObservationMatchesEdge(observation, edge) ||
!this.sourceUsableForQuality(edge)
) {
return null;
}
return {
eligibleWindows: observation.eligibleWindows,
eligibleDurationMs: observation.eligibleDurationMs,
freezeWindows: observation.freezeWindows,
freezeCount: observation.freezeCount,
freezeDurationMs: observation.freezeDurationMs,
pauseCount: observation.pauseCount,
pauseDurationMs: observation.pauseDurationMs,
};
}
private qualityUpstreamMatches(
edge: CommittedEdge<Resource>,
upstream: RouteQualityEvidenceInput["upstream"],
): boolean {
return edge.kind === "peer"
? upstream.kind === "peer" && upstream.peerId === edge.parentPeerId
: upstream.kind === "sfu";
}
private qualityObservationMatchesEdge(
observation: RouteQualityObservation,
edge: CommittedEdge<Resource>,
): boolean {
return edge.kind === "peer"
? observation.upstreamKind === "peer" &&
observation.upstreamPeerId === edge.parentPeerId
: observation.upstreamKind === "sfu" &&
observation.upstreamPeerId === null;
}
private sourceUsableForQuality(edge: CommittedEdge<Resource>): boolean {
if (edge.kind === "peer") {
return (
edge.parentSessionId ===
this.participants.get(edge.parentPeerId)?.sessionId &&
this.sourceUsable(edge.parentPeerId)
);
}
return Boolean(
this.hostPublication?.usable &&
this.hostPublication.physicalActive &&
this.hostPublication.generation === edge.publicationGeneration,
);
}
private clearQualityForParticipant(peerId: string): void {
const affected =
peerId === this.options.hostPeerId
? new Set(this.upstreamByViewer.keys())
: this.descendantsOf(peerId).add(peerId);
affected.forEach((childPeerId) => {
this.qualityObservations.delete(childPeerId);
if (this.upstreamByViewer.has(childPeerId)) {
this.qualityBaselinesPending.add(childPeerId);
} else {
this.qualityBaselinesPending.delete(childPeerId);
}
});
}
private clearSfuQuality(): void {
for (const [childPeerId, edge] of this.upstreamByViewer) {
if (edge.kind === "sfu") {
this.clearQualityForParticipant(childPeerId);
}
}
}
private recordDemand(
childPeerId: string,
nowMs: number,
@@ -2550,6 +2820,7 @@ export class RoomRouteController<Resource = unknown> {
for (const [viewerPeerId, edge] of this.upstreamByViewer) {
if (edge.kind !== "sfu" || edge.publicationGeneration !== generation) continue;
if (edge.physicalActive) released.push(edge.resource);
this.clearQualityForParticipant(viewerPeerId);
this.upstreamByViewer.delete(viewerPeerId);
}
if (this.hostPublication?.generation === generation) {
@@ -2658,6 +2929,16 @@ function elapsedMs(startedAtMs: number, endedAtMs: number): number | null {
return Number.isSafeInteger(elapsed) && elapsed >= 0 ? elapsed : null;
}
function safeAdd(total: number, delta: number): number {
const rounded = Math.round(delta);
if (!Number.isSafeInteger(rounded) || rounded < 0) {
return total;
}
return rounded > Number.MAX_SAFE_INTEGER - total
? Number.MAX_SAFE_INTEGER
: total + rounded;
}
function tupleKey(tuple: CandidateTuple): string {
return tuple.kind === "peer" ? `peer:${tuple.parentPeerId}` : `sfu:${tuple.publication}`;
}
+55 -25
View File
@@ -70,13 +70,11 @@ interface SocketState {
interface ViewerQualityEvidenceGate {
viewerSessionId: string;
recipientSessionId: string;
upstreamKind: "peer" | "sfu";
upstreamPeerId: string | null;
connectionId: string;
routeRevision: number;
presentationEpoch: number;
sequence: number;
acceptedAtMs: number;
}
interface ViewerMediaReadyState {
@@ -119,6 +117,7 @@ export class SignalingServer {
string,
ViewerQualityEvidenceGate
>();
private readonly viewerQualityEvidenceAttemptAtMs = new Map<string, number>();
private readonly viewerMediaReadyByRoom = new Map<
string,
Map<string, ViewerMediaReadyState>
@@ -252,6 +251,7 @@ export class SignalingServer {
routeCloseError = error;
}
this.viewerQualityEvidenceGates.clear();
this.viewerQualityEvidenceAttemptAtMs.clear();
this.viewerMediaReadyByRoom.clear();
this.shareGenerationsByRoom.clear();
this.pausedShareGenerationsByRoom.clear();
@@ -608,9 +608,12 @@ export class SignalingServer {
viewerPresence: message.viewerPresence === true,
};
if (participant.role === "viewer") {
this.viewerQualityEvidenceGates.delete(
viewerConnectionKey(participant.roomId, participant.peerId),
const viewerKey = viewerConnectionKey(
participant.roomId,
participant.peerId,
);
this.viewerQualityEvidenceGates.delete(viewerKey);
this.viewerQualityEvidenceAttemptAtMs.delete(viewerKey);
}
this.clearViewerGrace(participant.roomId, participant.peerId);
const routeParticipant = {
@@ -1139,9 +1142,19 @@ export class SignalingServer {
return;
}
const connectionId = this.connectionIdsByViewer.get(
viewerConnectionKey(source.roomId, source.peerId),
);
const gateKey = viewerConnectionKey(source.roomId, source.peerId);
const now = this.now();
const previousAttemptAtMs =
this.viewerQualityEvidenceAttemptAtMs.get(gateKey);
if (
previousAttemptAtMs !== undefined &&
now - previousAttemptAtMs < VIEWER_QUALITY_EVIDENCE_INTERVAL_MS
) {
return;
}
this.viewerQualityEvidenceAttemptAtMs.set(gateKey, now);
const connectionId = this.connectionIdsByViewer.get(gateKey);
if (!connectionId || message.guard.connectionId !== connectionId) {
return;
}
@@ -1179,22 +1192,19 @@ export class SignalingServer {
return;
}
const gateKey = viewerConnectionKey(source.roomId, source.peerId);
const previous = this.viewerQualityEvidenceGates.get(gateKey);
const sameGeneration =
const sameEdge =
previous?.viewerSessionId === viewerState.sessionId &&
previous.recipientSessionId === recipient.sessionId &&
previous.upstreamKind === upstream.kind &&
previous.upstreamPeerId ===
(upstream.kind === "peer" ? upstream.peerId : null) &&
previous.connectionId === connectionId &&
previous.routeRevision === routeRevision;
const now = this.now();
previous.connectionId === connectionId;
if (
sameGeneration &&
sameEdge &&
previous &&
(message.sequence <= previous.sequence ||
now - previous.acceptedAtMs < VIEWER_QUALITY_EVIDENCE_INTERVAL_MS)
(message.guard.presentationEpoch < previous.presentationEpoch ||
(message.guard.presentationEpoch === previous.presentationEpoch &&
message.sequence <= previous.sequence))
) {
return;
}
@@ -1209,29 +1219,42 @@ export class SignalingServer {
guard: {
connectionId,
routeRevision,
presentationEpoch: message.guard.presentationEpoch,
},
sequence: message.sequence,
windowMs: message.windowMs,
metrics: message.metrics,
};
const encoded = JSON.stringify(forwarded);
if (
Buffer.byteLength(encoded, "utf8") >
MAX_VIEWER_QUALITY_EVIDENCE_BYTES ||
!this.sendEncodedToSession(recipient.sessionId, encoded)
) {
if (Buffer.byteLength(encoded, "utf8") > MAX_VIEWER_QUALITY_EVIDENCE_BYTES) {
return;
}
const qualityResult = this.isHybridMediaEnabled()
? (this.hybridMediaRouter?.observeQualityEvidence({
roomId: source.roomId,
childPeerId: source.peerId,
childSessionId: viewerState.sessionId,
routeRevision,
connectionId,
upstream,
presentationEpoch: message.guard.presentationEpoch,
windowMs: message.windowMs,
metrics: message.metrics,
acceptedAtMs: now,
}) ?? "rejected")
: "accepted";
if (qualityResult === "rejected") {
return;
}
this.viewerQualityEvidenceGates.set(gateKey, {
viewerSessionId: viewerState.sessionId,
recipientSessionId: recipient.sessionId,
upstreamKind: upstream.kind,
upstreamPeerId: upstream.kind === "peer" ? upstream.peerId : null,
connectionId,
routeRevision,
presentationEpoch: message.guard.presentationEpoch,
sequence: message.sequence,
acceptedAtMs: now,
});
this.sendEncodedToSession(recipient.sessionId, encoded);
if (!host || host.sessionId === recipient.sessionId) {
return;
}
@@ -1976,6 +1999,11 @@ export class SignalingServer {
this.viewerQualityEvidenceGates.delete(key);
}
}
for (const key of this.viewerQualityEvidenceAttemptAtMs.keys()) {
if (key.startsWith(prefix)) {
this.viewerQualityEvidenceAttemptAtMs.delete(key);
}
}
}
private setViewerConnectionId(
@@ -1986,6 +2014,7 @@ export class SignalingServer {
const key = viewerConnectionKey(roomId, viewerPeerId);
if (this.connectionIdsByViewer.get(key) !== connectionId) {
this.viewerQualityEvidenceGates.delete(key);
this.viewerQualityEvidenceAttemptAtMs.delete(key);
}
this.connectionIdsByViewer.set(key, connectionId);
}
@@ -1997,6 +2026,7 @@ export class SignalingServer {
const key = viewerConnectionKey(roomId, viewerPeerId);
this.connectionIdsByViewer.delete(key);
this.viewerQualityEvidenceGates.delete(key);
this.viewerQualityEvidenceAttemptAtMs.delete(key);
}
}
+43 -21
View File
@@ -6,7 +6,7 @@ import { isCanonicalVideoCodecEvidence } from "./video-codec-evidence.js";
export const MAX_VIEWERS_PER_ROOM_LIMIT = 20;
export const MAX_PARTICIPANTS_PER_ROOM_LIMIT = MAX_VIEWERS_PER_ROOM_LIMIT + 1;
export const MAX_SIGNAL_BYTES = 64 * 1024;
export const SIGNALING_PROTOCOL = "screener-v12";
export const SIGNALING_PROTOCOL = "screener-v13";
export const SIGNAL_CLOSE_CODES = {
serviceRestart: 1012,
sessionReplaced: 4001,
@@ -397,6 +397,22 @@ const routeDiagnosticParentSchema = z.discriminatedUnion("kind", [
})
.strict(),
]);
const routeDiagnosticQualityValueSchema = z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER);
const routeDiagnosticQualitySchema = z
.object({
eligibleWindows: routeDiagnosticQualityValueSchema,
eligibleDurationMs: routeDiagnosticQualityValueSchema,
freezeWindows: routeDiagnosticQualityValueSchema,
freezeCount: routeDiagnosticQualityValueSchema,
freezeDurationMs: routeDiagnosticQualityValueSchema,
pauseCount: routeDiagnosticQualityValueSchema,
pauseDurationMs: routeDiagnosticQualityValueSchema,
})
.strict();
const routeDiagnosticChildSchema = z
.object({
ordinal: routeDiagnosticOrdinalSchema,
@@ -418,6 +434,7 @@ const routeDiagnosticChildSchema = z
finalMs: routeDiagnosticDurationSchema,
finalRoute: routeDiagnosticFinalRouteSchema,
rejectionBucket: routeDiagnosticRejectionBucketSchema,
quality: routeDiagnosticQualitySchema.nullable(),
})
.strict();
const routeDiagnosticOperationSchema = z
@@ -524,6 +541,20 @@ const nullableSignedEvidenceNumber = (absoluteMaximum: number) =>
const nullableEvidenceInteger = (maximum: number) =>
z.number().int().min(0).max(maximum).nullable();
const nullableSafeEvidenceNumber = z
.number()
.finite()
.min(0)
.max(Number.MAX_SAFE_INTEGER)
.nullable();
const nullableSafeEvidenceInteger = z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER)
.nullable();
export const viewerQualityEvidenceMetricsSchema = z
.object({
width: z.number().int().min(1).max(16_384).nullable(),
@@ -537,8 +568,10 @@ export const viewerQualityEvidenceMetricsSchema = z
framesDecodedDelta: nullableEvidenceInteger(10_000),
framesDroppedDelta: nullableEvidenceInteger(10_000),
decodeMsPerFrame: nullableEvidenceNumber(60_000),
freezeCountDelta: nullableEvidenceInteger(10_000),
freezeDurationMsDelta: nullableEvidenceNumber(5_000),
freezeCountDelta: nullableSafeEvidenceInteger,
freezeDurationMsDelta: nullableSafeEvidenceNumber,
pauseCountDelta: nullableSafeEvidenceInteger,
pauseDurationMsDelta: nullableSafeEvidenceNumber,
codec: z
.string()
.max(64)
@@ -587,6 +620,11 @@ const viewerQualityEvidenceGuardSchema = z
.object({
connectionId: opaqueIdSchema,
routeRevision: mediaRouteRevisionSchema,
presentationEpoch: z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER),
})
.strict();
@@ -596,26 +634,13 @@ const viewerQualityEvidenceWindowShape = {
metrics: viewerQualityEvidenceMetricsSchema,
};
function freezeFitsEvidenceWindow(value: {
windowMs: number;
metrics: ViewerQualityEvidenceMetrics;
}): boolean {
return (
value.metrics.freezeDurationMsDelta === null ||
value.metrics.freezeDurationMsDelta <= value.windowMs
);
}
export const viewerQualityEvidenceMessageSchema = z
.object({
type: z.literal("viewer-quality-evidence"),
guard: viewerQualityEvidenceGuardSchema,
...viewerQualityEvidenceWindowShape,
})
.strict()
.refine(freezeFitsEvidenceWindow, {
message: "Viewer freeze duration exceeds its evidence window",
});
.strict();
const authenticateMessageSchema = z.discriminatedUnion("role", [
z
@@ -937,10 +962,7 @@ export const serverMessageSchema = z.union([
guard: viewerQualityEvidenceGuardSchema,
...viewerQualityEvidenceWindowShape,
})
.strict()
.refine(freezeFitsEvidenceWindow, {
message: "Viewer freeze duration exceeds its evidence window",
}),
.strict(),
z
.object({
type: z.literal("host-status"),