fix(media): reselect persistently low-fps relay edges [skip ci]
This commit is contained in:
1 parent
d94bac11ca
commit
ed57d3ed5e
6 files changed
+254
-16
No files matched your search
@@ -169,8 +169,10 @@ current connection, using the existing finite monotonic stats timestamp; its
|
||||
DOMHighResTimeStamp-derived window is rounded before the one-to-five-second
|
||||
bound is checked. Viewer C may wait pending, but the router advances the streak
|
||||
only when that C has a hard receive predicate and B independently reports one
|
||||
of the two hard sender predicates. Both inputs are WebRTC stats gathered by
|
||||
stock browsers at separate endpoints; parent remote loss originates in RTCP.
|
||||
of the two hard sender predicates. Signaling binds the room's current
|
||||
`maxFramerate` to C internally without changing the public wire. Both inputs
|
||||
are WebRTC stats gathered by stock browsers at separate endpoints; parent
|
||||
remote loss originates in RTCP.
|
||||
This is cross-endpoint corroboration, not cryptographic independence or defense
|
||||
against colluding authenticated participants. Raw stats, addresses, candidates,
|
||||
SDP, URLs and a composite score remain out of wire.
|
||||
@@ -379,7 +381,8 @@ The first controller reacts only to discrete events:
|
||||
- three consecutive, current-identity correlated Viewer C/parent B windows in
|
||||
which C has a hard receive predicate (freeze duration at least 50% of the
|
||||
sample window, positive received RTP with zero decoded frames, or at least
|
||||
100 received-plus-lost packets with loss at least 30%) and B independently
|
||||
100 received-plus-lost packets with loss at least 30%, or decoded FPS below
|
||||
five-sixths of the room's current `maxFramerate`) and B independently
|
||||
reports `cpu`/`bandwidth` sender limitation or at least 100 sent packets with
|
||||
RTCP-reported remote loss divided by sent packets at least 30%.
|
||||
|
||||
@@ -402,10 +405,10 @@ release only the quality-owned temporary parent exclusion; a real
|
||||
`route-failed` exclusion remains owned by the failure path. The W3C stats
|
||||
definitions establish the counter meanings and their WebRTC 1.0 example uses
|
||||
30% loss as a likely culprit; they do not
|
||||
prescribe Screener's route policy. Therefore 50%, 100 packets, three windows,
|
||||
five seconds and 30 seconds are conservative candidate constants pending
|
||||
production calibration, not claimed optimums. Measurements may change them
|
||||
only through a reviewed change.
|
||||
prescribe Screener's route policy. Therefore 50% freeze share, five-sixths FPS,
|
||||
100 packets, three windows, five seconds and 30 seconds are conservative
|
||||
candidate constants pending production calibration, not claimed optimums.
|
||||
Measurements may change them only through a reviewed change.
|
||||
ADR-0007's `HIGH`/`FALLBACK` quality state is separate and does not become a
|
||||
topology trigger or a room-wide health score.
|
||||
|
||||
|
||||
@@ -196,14 +196,16 @@ server accepts one B from the bound parent session and generation. The router
|
||||
may retain C while awaiting B, but advances only when the correlated pair has a
|
||||
hard C receive predicate and a hard B sender predicate. `sending`, either
|
||||
report alone, and healthy or ambiguous pairs reset or do not advance the
|
||||
streak.
|
||||
streak. Signaling supplies the room's current `maxFramerate` only to the
|
||||
server-side router alongside C; no public evidence field is added.
|
||||
It holds at most one pending/streak state per connected Viewer and one room
|
||||
cooldown, with no timer, weighted score or global parent-capacity decision.
|
||||
|
||||
The conservative Viewer C hard predicates are: freeze duration at least half
|
||||
of the one-to-five-second window; positive received-packet delta with zero
|
||||
decoded frames; or at least 100 received-plus-lost packets with loss at least
|
||||
30%. Parent B must independently report the exact `cpu`/`bandwidth` sender
|
||||
30%; or decoded FPS below five-sixths of the room's current `maxFramerate`.
|
||||
Parent B must independently report the exact `cpu`/`bandwidth` sender
|
||||
limitation or at least 100 sent packets with remote loss divided by sent packets
|
||||
at least 30%. Three consecutive dual-hard-bad windows are required. A healthy
|
||||
or incomplete correlated window, any Viewer/parent session, connection, route
|
||||
@@ -233,9 +235,9 @@ Viewer-side
|
||||
`packetsReceived`, `packetsLost`, `framesDecoded`, `freezeCount` and
|
||||
`totalFreezesDuration`; its WebRTC 1.0 diagnostic example treats loss over 30%
|
||||
as a likely culprit. It does not define route-migration thresholds. The 50%
|
||||
freeze share, 100-packet floor, three windows, five-second gap and 30-second
|
||||
cooldown are falsifiable candidate constants for production calibration, not
|
||||
standards-derived or claimed optimum values.
|
||||
freeze share, five-sixths FPS ratio, 100-packet floor, three windows,
|
||||
five-second gap and 30-second cooldown are falsifiable candidate constants for
|
||||
production calibration, not standards-derived or claimed optimum values.
|
||||
|
||||
The correlated pair can therefore attribute a problem only to the current
|
||||
parent-to-child edge generation. It cannot prove that the parent is globally
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ Parallel flagship work; use minimum relevant checks and rollback proof. Target-d
|
||||
- Silent partitions can wait 30 to 60 seconds for heartbeat detection before the default 5-second grace; this remains unverified.
|
||||
- Production startup blur remains open. One local Chrome synthetic same-`balanced` A/B kept route/PC/SSRC/track and coincided with 720p -> 1080p without loss/freeze, but natural ramp prevents causal, production, or SFU proof.
|
||||
- ADR-0006 has one Viewer proof; two-edge/FIFO, hardware, endurance, public/downloaded-package, and browser-diversity proof remain open.
|
||||
- Production fanout is Host2/Viewer2 and rejects child3. Relay resource behavior remains open.
|
||||
- Production fanout is Host2/Viewer2 and rejects child3. C+B moves only the affected subtree; relay-capacity penalties stay a separate P2.
|
||||
- Active SFU already publishes `q,h`; only BWE/resource/zero-child/root gates are unverified. No local per-leaf UDP shaper; resume with Linux `tc` or public canary, never CDP. Web P2P/SVC shortcuts remain no-go.
|
||||
- Access lacks headful/production proof for fragment consumption, rotate/revoke and request/nginx/journal/SQLite leakage; its source gate is loopback-only. No accounts.
|
||||
- Split large modules only at proven consumer boundaries, never by file length.
|
||||
|
||||
@@ -33,6 +33,7 @@ const VIEWER_QUALITY_BAD_WINDOWS_TO_REASSIGN = 3;
|
||||
const VIEWER_QUALITY_EVIDENCE_GAP_MS = 5_000;
|
||||
const VIEWER_QUALITY_REASSIGN_COOLDOWN_MS = 30_000;
|
||||
const VIEWER_QUALITY_FREEZE_RATIO = 0.5;
|
||||
const VIEWER_QUALITY_MIN_FPS_RATIO = 5 / 6;
|
||||
const VIEWER_QUALITY_MIN_LOSS_PACKETS = 100;
|
||||
const VIEWER_QUALITY_HIGH_LOSS_RATIO = 0.3;
|
||||
interface PendingRoutePreparation {
|
||||
@@ -122,6 +123,7 @@ interface ViewerQualityEvidenceState {
|
||||
badWindowCount: number;
|
||||
pendingViewerEvidence: {
|
||||
acceptedAtMs: number;
|
||||
maxFramerate: number;
|
||||
evidence: Extract<ServerMessage, { type: "viewer-quality-evidence" }>;
|
||||
} | null;
|
||||
}
|
||||
@@ -130,6 +132,7 @@ interface ForwardedViewerQualityEvidence {
|
||||
roomId: string;
|
||||
viewerSessionId: string;
|
||||
parentSessionId: string;
|
||||
maxFramerate: number;
|
||||
evidence: Extract<ServerMessage, { type: "viewer-quality-evidence" }>;
|
||||
}
|
||||
|
||||
@@ -496,7 +499,13 @@ export class HybridMediaRouter {
|
||||
}
|
||||
|
||||
handleViewerQualityEvidence(input: ForwardedViewerQualityEvidence): void {
|
||||
const { evidence, roomId, viewerSessionId, parentSessionId } = input;
|
||||
const {
|
||||
evidence,
|
||||
roomId,
|
||||
viewerSessionId,
|
||||
parentSessionId,
|
||||
maxFramerate,
|
||||
} = input;
|
||||
const { viewerPeerId, parentPeerId } = evidence;
|
||||
const viewer = this.options.roomStore.getConnectedViewer(
|
||||
roomId,
|
||||
@@ -552,7 +561,7 @@ export class HybridMediaRouter {
|
||||
? previous.lastCorrelatedAtMs
|
||||
: null,
|
||||
badWindowCount: canContinueStreak ? previous.badWindowCount : 0,
|
||||
pendingViewerEvidence: { acceptedAtMs: now, evidence },
|
||||
pendingViewerEvidence: { acceptedAtMs: now, maxFramerate, evidence },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -626,7 +635,10 @@ export class HybridMediaRouter {
|
||||
}
|
||||
|
||||
const hardBad =
|
||||
isHardBadViewerQualityWindow(pending.evidence) &&
|
||||
isHardBadViewerQualityWindow(
|
||||
pending.evidence,
|
||||
pending.maxFramerate,
|
||||
) &&
|
||||
isHardBadParentEdgeQualityProof(evidence.proof);
|
||||
state.badWindowCount = hardBad
|
||||
? state.lastCorrelatedAtMs !== null &&
|
||||
@@ -3205,6 +3217,7 @@ function sameUpstream(
|
||||
|
||||
function isHardBadViewerQualityWindow(
|
||||
evidence: Extract<ServerMessage, { type: "viewer-quality-evidence" }>,
|
||||
maxFramerate: number,
|
||||
): boolean {
|
||||
const metrics = evidence.metrics;
|
||||
const freezeRatio =
|
||||
@@ -3221,6 +3234,12 @@ function isHardBadViewerQualityWindow(
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
metrics.framesPerSecond !== null &&
|
||||
metrics.framesPerSecond < maxFramerate * VIEWER_QUALITY_MIN_FPS_RATIO
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
metrics.packetsReceivedDelta === null ||
|
||||
metrics.packetsLostDelta === null
|
||||
|
||||
@@ -1122,6 +1122,9 @@ export class SignalingServer {
|
||||
roomId: source.roomId,
|
||||
viewerSessionId: viewerState.sessionId,
|
||||
parentSessionId: parent.sessionId,
|
||||
maxFramerate:
|
||||
(this.qualitySettingsByRoom.get(source.roomId) ??
|
||||
DEFAULT_QUALITY_SETTINGS).maxFramerate,
|
||||
evidence: forwarded,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,6 +282,62 @@ async function correlateParentEdgeQualityEvidence(
|
||||
return viewerEvidence;
|
||||
}
|
||||
|
||||
async function startPeerQualityReselectionFixture(
|
||||
prefix: string,
|
||||
now: () => number,
|
||||
) {
|
||||
const harness = await startHarness({
|
||||
peerAssistedMedia: true,
|
||||
now,
|
||||
});
|
||||
const host = await openClient(harness.webSocketUrl);
|
||||
const hostAuth = peerAssisted(
|
||||
await authenticate(host, harness.room, "host", `${prefix}-host`),
|
||||
);
|
||||
const alternateParent = await openClient(harness.webSocketUrl);
|
||||
const alternateParentAuth = peerAssisted(
|
||||
await authenticate(
|
||||
alternateParent,
|
||||
harness.room,
|
||||
"viewer",
|
||||
`${prefix}-alternate`,
|
||||
),
|
||||
);
|
||||
const viewer = await openClient(harness.webSocketUrl);
|
||||
const viewerAuth = peerAssisted(
|
||||
await authenticate(viewer, harness.room, "viewer", `${prefix}-viewer`),
|
||||
);
|
||||
expect(alternateParentAuth.routeAssignment.upstream).toEqual({
|
||||
kind: "peer",
|
||||
peerId: hostAuth.peerId,
|
||||
});
|
||||
expect(viewerAuth.routeAssignment.upstream).toEqual({
|
||||
kind: "peer",
|
||||
peerId: hostAuth.peerId,
|
||||
});
|
||||
|
||||
const connectionId = `${prefix}_connection`;
|
||||
host.socket.send(
|
||||
JSON.stringify({
|
||||
type: "signal",
|
||||
targetPeerId: viewerAuth.peerId,
|
||||
payload: {
|
||||
kind: "description",
|
||||
connectionId,
|
||||
description: { type: "offer", sdp: "v=0\r\n" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await viewer.inbox.next("signal");
|
||||
return {
|
||||
alternateParentAuth,
|
||||
connectionId,
|
||||
host,
|
||||
viewer,
|
||||
viewerAuth,
|
||||
};
|
||||
}
|
||||
|
||||
async function startSfuHarness(options: {
|
||||
tokenIssuer: SfuTokenIssuer;
|
||||
selectedEdgeTurn?: boolean;
|
||||
@@ -3005,6 +3061,161 @@ describe("WebSocket signaling", () => {
|
||||
acceptedParentEvidence.mockRestore();
|
||||
});
|
||||
|
||||
it("requires hard parent proof before low-FPS windows reparent", async () => {
|
||||
let now = 90_000;
|
||||
const fixture = await startPeerQualityReselectionFixture(
|
||||
"quality_low_fps_proof",
|
||||
() => now,
|
||||
);
|
||||
let sequence = 0;
|
||||
const sendWindow = async (
|
||||
framesPerSecond: number,
|
||||
proof: ParentEdgeQualityProof,
|
||||
) => {
|
||||
fixture.viewer.socket.send(
|
||||
JSON.stringify(
|
||||
viewerQualityEvidenceWithMetrics(
|
||||
fixture.connectionId,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
sequence,
|
||||
{ framesPerSecond },
|
||||
),
|
||||
),
|
||||
);
|
||||
sequence += 1;
|
||||
await correlateParentEdgeQualityEvidence(fixture.host, proof);
|
||||
now += 2_000;
|
||||
};
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
await sendWindow(40, { kind: "sending", packetsSentDelta: 1_500 });
|
||||
}
|
||||
await expect(
|
||||
fixture.viewer.inbox.next("route-update", 40),
|
||||
).rejects.toThrow("Timed out");
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
await sendWindow(49, {
|
||||
kind: "sender-limited",
|
||||
packetsSentDelta: 1_500,
|
||||
reason: "bandwidth",
|
||||
});
|
||||
}
|
||||
const moved = await nextActiveRouteAfter(
|
||||
fixture.viewer,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
);
|
||||
expect(moved.assignment.upstream).toEqual({
|
||||
kind: "peer",
|
||||
peerId: fixture.alternateParentAuth.peerId,
|
||||
});
|
||||
});
|
||||
|
||||
it("resets a low-FPS streak at five-sixths of maxFramerate", async () => {
|
||||
let now = 120_000;
|
||||
const fixture = await startPeerQualityReselectionFixture(
|
||||
"quality_low_fps_reset",
|
||||
() => now,
|
||||
);
|
||||
let sequence = 0;
|
||||
const sendWindow = async (framesPerSecond: number) => {
|
||||
fixture.viewer.socket.send(
|
||||
JSON.stringify(
|
||||
viewerQualityEvidenceWithMetrics(
|
||||
fixture.connectionId,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
sequence,
|
||||
{ framesPerSecond },
|
||||
),
|
||||
),
|
||||
);
|
||||
sequence += 1;
|
||||
await correlateParentEdgeQualityEvidence(fixture.host, {
|
||||
kind: "sender-limited",
|
||||
packetsSentDelta: 1_500,
|
||||
reason: "cpu",
|
||||
});
|
||||
now += 2_000;
|
||||
};
|
||||
|
||||
await sendWindow(49);
|
||||
await sendWindow(49);
|
||||
await sendWindow(50);
|
||||
await sendWindow(49);
|
||||
await sendWindow(49);
|
||||
await expect(
|
||||
fixture.viewer.inbox.next("route-update", 40),
|
||||
).rejects.toThrow("Timed out");
|
||||
|
||||
await sendWindow(49);
|
||||
const moved = await nextActiveRouteAfter(
|
||||
fixture.viewer,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
);
|
||||
expect(moved.assignment.upstream).toEqual({
|
||||
kind: "peer",
|
||||
peerId: fixture.alternateParentAuth.peerId,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the current room maxFramerate for each low-FPS window", async () => {
|
||||
let now = 150_000;
|
||||
const fixture = await startPeerQualityReselectionFixture(
|
||||
"quality_low_fps_profile",
|
||||
() => now,
|
||||
);
|
||||
let sequence = 0;
|
||||
const sendWindow = async (framesPerSecond: number) => {
|
||||
fixture.viewer.socket.send(
|
||||
JSON.stringify(
|
||||
viewerQualityEvidenceWithMetrics(
|
||||
fixture.connectionId,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
sequence,
|
||||
{ framesPerSecond },
|
||||
),
|
||||
),
|
||||
);
|
||||
sequence += 1;
|
||||
await correlateParentEdgeQualityEvidence(fixture.host, {
|
||||
kind: "sender-limited",
|
||||
packetsSentDelta: 1_500,
|
||||
reason: "bandwidth",
|
||||
});
|
||||
now += 2_000;
|
||||
};
|
||||
|
||||
await sendWindow(49);
|
||||
await sendWindow(49);
|
||||
fixture.host.socket.send(
|
||||
JSON.stringify({
|
||||
type: "set-quality-settings",
|
||||
qualitySettings: balancedQualitySettings,
|
||||
}),
|
||||
);
|
||||
expect(await fixture.viewer.inbox.next("quality-settings")).toEqual({
|
||||
type: "quality-settings",
|
||||
qualitySettings: balancedQualitySettings,
|
||||
});
|
||||
|
||||
await sendWindow(29);
|
||||
await sendWindow(24);
|
||||
await sendWindow(24);
|
||||
await expect(
|
||||
fixture.viewer.inbox.next("route-update", 40),
|
||||
).rejects.toThrow("Timed out");
|
||||
|
||||
await sendWindow(24);
|
||||
const moved = await nextActiveRouteAfter(
|
||||
fixture.viewer,
|
||||
fixture.viewerAuth.routeRevision,
|
||||
);
|
||||
expect(moved.assignment.upstream).toEqual({
|
||||
kind: "peer",
|
||||
peerId: fixture.alternateParentAuth.peerId,
|
||||
});
|
||||
});
|
||||
|
||||
it("reparents only a viewer subtree after three hard quality windows", async () => {
|
||||
let now = 100_000;
|
||||
const harness = await startHarness({
|
||||
|
||||
Reference in new issue
Block a user