fix(room): preserve Viewer capacity across sharing generations (#307)

This commit is contained in:
AAA authored and GitHub committed 2026-08-28 01:46:32 +08:00
1 parent a39b597396
commit 42e756e00a
7 files changed
+60 -16

No files matched your search

@@ -31,6 +31,10 @@ copy cap `C`: default `2`, configurable only as `1`, `2`, or `3`.
- a client may advertise only `0..C` currently available copies, producing the
controller's effective capacity.
That advertised capacity belongs to the connected endpoint, not one sharing
generation. Replacing the room graph retains it; Viewer departure or room
deletion clears it.
One transition-overlap reservation may temporarily raise a producer to
`min(C + 1, 3)`. Commit or abort releases it. A fourth endpoint copy is never
authorized. SFU ingress and subscription egress are independent server resources
-10
View File
@@ -821,15 +821,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) {
commitViewerQualityEvidence(peerId, null);
}
function clearAllViewerQualityEvidence(): void {
viewerQualityEvidenceTimersRef.current.forEach((timer) =>
window.clearTimeout(timer),
);
viewerQualityEvidenceTimersRef.current.clear();
viewerQualityEvidenceRef.current = new Map();
setViewerQualityEvidence(new Map());
}
function acceptViewerQualityEvidence(evidence: ViewerQualityEvidence): void {
const peer = peersRef.current.get(evidence.viewerPeerId);
const directSnapshot = peer?.getSnapshot() ?? null;
@@ -1395,7 +1386,6 @@ export function HostPage({ onAuthorizationRequired }: HostPageProps = {}) {
setViewerPasswordEnabled(message.viewerPasswordEnabled);
setViewerPasswordDraft(authenticatedProfile.roomPassword ?? "");
setViewerPasswordVisible(false);
clearAllViewerQualityEvidence();
setSfuStandbyUrl(
"sfuStandbyUrl" in message ? message.sfuStandbyUrl : null,
);
+2 -2
View File
@@ -682,9 +682,9 @@ export class HostPeer {
}
switch (this.limitationReason) {
case "bandwidth":
return "此发送连接持续按带宽估计降低分辨率或帧率";
return "当前连接带宽受限,画质已自动降低";
case "cpu":
return "此发送连接持续因编码性能降低分辨率或帧率";
return "编码性能受限,画质已自动降低";
case "other":
return "浏览器持续报告其他画质限制";
default:
+11
View File
@@ -918,11 +918,22 @@ export class HybridMediaRouter {
this.options.deleteConnectionId(roomId, peerId);
if (room?.controller?.confirmDeparture(peerId, this.now())) {
this.requestPump(roomId);
} else if (room && !room.controller && room.advertisedCapacityByViewer.size === 0) {
this.rooms.delete(roomId);
}
}
stopRoom(roomId: string): void {
const capacities = new Map(
this.rooms.get(roomId)?.advertisedCapacityByViewer ?? [],
);
this.clearRoom(roomId);
if (capacities.size > 0) {
const room = this.room(roomId);
for (const [peerId, capacity] of capacities) {
room.advertisedCapacityByViewer.set(peerId, capacity);
}
}
}
deleteRoom(roomId: string): void {
+3 -3
View File
@@ -23,10 +23,10 @@ describe("progressive connection details", () => {
expect(
qualityLimitationSummary([
snapshot(null),
snapshot("此发送连接持续按带宽估计降低分辨率或帧率"),
snapshot("此发送连接持续因编码性能降低分辨率或帧率"),
snapshot("当前连接带宽受限,画质已自动降低"),
snapshot("编码性能受限,画质已自动降低"),
]),
).toBe("此发送连接持续按带宽估计降低分辨率或帧率");
).toBe("当前连接带宽受限,画质已自动降低");
expect(qualityLimitationSummary([snapshot(null)])).toBeNull();
});
+3 -1
View File
@@ -1379,7 +1379,9 @@ describe("HostPeer source replacement", () => {
await sample();
expect(updates.at(-1)?.qualityWarning).toBeNull();
await sample();
expect(updates.at(-1)?.qualityWarning).toContain("按带宽估计");
expect(updates.at(-1)?.qualityWarning).toBe(
"当前连接带宽受限,画质已自动降低",
);
expect(sender.setParameters).toHaveBeenCalledTimes(2);
await sample();
+37
View File
@@ -1326,6 +1326,43 @@ describe("HybridMediaRouter v9 runtime", () => {
await router.close();
});
it("preserves connected Viewer capacity across sharing generations", async () => {
const { store, sent, router } = harness(1);
const room = await store.createRoom();
const host = connectHost(store, room);
complete(router, host);
const root = connectViewer(store, room, "retained-capacity-root");
complete(router, root);
await vi.waitFor(() => expect(preparedFor(sent, root.sessionId)).toBeDefined());
const initialRoot = preparedFor(sent, root.sessionId)!;
router.handleRouteReady(root, {
type: "route-ready",
revision: initialRoot.revision,
phase: "prepare",
});
router.setViewerRelayCapacity(root, 1);
router.stopRoom(room.roomId);
sent.set(root.sessionId, []);
complete(router, host);
await vi.waitFor(() => expect(preparedFor(sent, root.sessionId)).toBeDefined());
const restartedRoot = preparedFor(sent, root.sessionId)!;
router.handleRouteReady(root, {
type: "route-ready",
revision: restartedRoot.revision,
phase: "prepare",
});
const child = connectViewer(store, room, "retained-capacity-child");
complete(router, child);
await vi.waitFor(() => expect(preparedFor(sent, child.sessionId)).toBeDefined());
expect(preparedFor(sent, child.sessionId)?.assignment.upstream).toEqual({
kind: "peer",
peerId: root.peerId,
});
await router.close();
});
it("emits typed exhaustion when reconciliation has no candidate", async () => {
const { store, sent, router } = harness(1);
const room = await store.createRoom();