feat: stream private office screens
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
fetchEphemeralIceConfiguration,
|
||||
IceConfigurationUnavailableError,
|
||||
} from "../media/iceClient.ts";
|
||||
|
||||
describe("ephemeral ICE configuration client", () => {
|
||||
it("posts the exact authenticated request and returns an in-memory RTC configuration", async () => {
|
||||
let input: RequestInfo | URL | undefined;
|
||||
let init: RequestInit | undefined;
|
||||
const result = await fetchEphemeralIceConfiguration({
|
||||
authenticatedFetch: async (nextInput, nextInit) => {
|
||||
input = nextInput;
|
||||
init = nextInit;
|
||||
return Response.json({
|
||||
type: "ice-config-grant",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-test-1",
|
||||
issuedAtMs: 100,
|
||||
expiresAtMs: 600_000,
|
||||
iceServers: [
|
||||
{ urls: ["stun:relay.example.test:3478"] },
|
||||
{
|
||||
urls: ["turns:relay.example.test:5349"],
|
||||
username: "temporary-user-01",
|
||||
credential: "temporary-password-01",
|
||||
credentialType: "password",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
now: () => 200,
|
||||
requestId: () => "ice-test-1",
|
||||
});
|
||||
assert.equal(input, "/api/v1/media/ice");
|
||||
assert.equal(init?.method, "POST");
|
||||
assert.equal(init?.credentials, "same-origin");
|
||||
assert.equal(init?.cache, "no-store");
|
||||
assert.deepEqual(JSON.parse(String(init?.body)), {
|
||||
type: "ice-config-request",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-test-1",
|
||||
});
|
||||
assert.deepEqual(result.configuration.iceServers, [
|
||||
{ urls: ["stun:relay.example.test:3478"] },
|
||||
{
|
||||
urls: ["turns:relay.example.test:5349"],
|
||||
username: "temporary-user-01",
|
||||
credential: "temporary-password-01",
|
||||
credentialType: "password",
|
||||
},
|
||||
]);
|
||||
assert.equal(result.expiresAtMs, 600_000);
|
||||
});
|
||||
|
||||
it("rejects unavailable, mismatched, expired, and malformed responses", async () => {
|
||||
const run = (body: unknown) => fetchEphemeralIceConfiguration({
|
||||
authenticatedFetch: async () => Response.json(body),
|
||||
now: () => 200,
|
||||
requestId: () => "ice-test-1",
|
||||
});
|
||||
await assert.rejects(run({
|
||||
type: "ice-config-unavailable",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-test-1",
|
||||
retryAfterMs: 10_000,
|
||||
}), /temporarily unavailable/);
|
||||
await assert.rejects(run({
|
||||
type: "ice-config-unavailable",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-test-2",
|
||||
retryAfterMs: 10_000,
|
||||
}), /did not match/);
|
||||
await assert.rejects(run({
|
||||
type: "ice-config-grant",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-test-1",
|
||||
issuedAtMs: 10,
|
||||
expiresAtMs: 100,
|
||||
iceServers: [{
|
||||
urls: ["turn:relay.example.test:3478"],
|
||||
username: "temporary-user-01",
|
||||
credential: "temporary-password-01",
|
||||
credentialType: "password",
|
||||
}],
|
||||
}), /not active/);
|
||||
await assert.rejects(run({ profile: "must-not-parse" }), /invalid response/);
|
||||
});
|
||||
|
||||
it("exposes validated 429 and 503 retry timing without exposing an untrusted body", async () => {
|
||||
for (const status of [429, 503]) {
|
||||
await assert.rejects(fetchEphemeralIceConfiguration({
|
||||
authenticatedFetch: async () => Response.json({
|
||||
type: "ice-config-unavailable",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-backoff-1",
|
||||
retryAfterMs: 12_345,
|
||||
}, { status }),
|
||||
requestId: () => "ice-backoff-1",
|
||||
}), (error: unknown) => {
|
||||
assert.ok(error instanceof IceConfigurationUnavailableError);
|
||||
assert.equal(error.retryAfterMs, 12_345);
|
||||
assert.equal(error.status, status);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
await assert.rejects(fetchEphemeralIceConfiguration({
|
||||
authenticatedFetch: async () => Response.json({
|
||||
type: "ice-config-unavailable",
|
||||
protocolVersion: 1,
|
||||
requestId: "somebody-elses-request",
|
||||
retryAfterMs: 12_345,
|
||||
}, { status: 503 }),
|
||||
requestId: () => "ice-backoff-1",
|
||||
}), /did not match/);
|
||||
|
||||
await assert.rejects(fetchEphemeralIceConfiguration({
|
||||
authenticatedFetch: async () => Response.json({ retryAfterMs: 1 }, { status: 503 }),
|
||||
requestId: () => "ice-backoff-1",
|
||||
}), /failed \(503\)/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
isIceConfigGrantActive,
|
||||
isSafeIceUrl,
|
||||
parseIceConfigRequest,
|
||||
parseIceConfigResponse,
|
||||
type IceConfigGrant,
|
||||
} from "../media/index.ts";
|
||||
|
||||
const grant = (): IceConfigGrant => ({
|
||||
type: "ice-config-grant",
|
||||
protocolVersion: 1,
|
||||
requestId: "ice-request-1",
|
||||
issuedAtMs: 1_000,
|
||||
expiresAtMs: 601_000,
|
||||
iceServers: [
|
||||
{ urls: ["stun:relay.example.test:3478"] },
|
||||
{
|
||||
urls: ["turn:relay.example.test:3478", "turns:relay.example.test:5349"],
|
||||
username: "1700000600:opaque_nonce_value",
|
||||
credential: "dGVzdF9jcmVkZW50aWFsX3ZhbHVl",
|
||||
credentialType: "password",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe("ICE configuration wire contract", () => {
|
||||
it("accepts exact requests and bounded ephemeral grants", () => {
|
||||
assert.equal(parseIceConfigRequest({
|
||||
type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1",
|
||||
}).ok, true);
|
||||
assert.equal(parseIceConfigResponse(JSON.parse(JSON.stringify(grant()))).ok, true);
|
||||
assert.equal(isIceConfigGrantActive(grant(), 300_000), true);
|
||||
assert.equal(isIceConfigGrantActive(grant(), 601_000), false);
|
||||
});
|
||||
|
||||
it("allows only credential-free ICE URL forms", () => {
|
||||
for (const url of [
|
||||
"stun:relay.example.test:3478", "stuns:relay.example.test:5349",
|
||||
"turn:192.0.2.4:3478", "turn:relay.example.test:3478?transport=udp",
|
||||
"turn:relay.example.test:3478?transport=tcp", "turns:[2001:db8::1]:5349?transport=tcp",
|
||||
]) {
|
||||
assert.equal(isSafeIceUrl(url), true, url);
|
||||
}
|
||||
for (const url of [
|
||||
"https://relay.example.test", "turn:user:pass@relay.example.test:3478",
|
||||
"turn:relay.example.test:3478?credential=secret", "turn://relay.example.test:3478",
|
||||
"stun:relay.example.test:3478?transport=udp", "turn:relay.example.test:3478?transport=sctp",
|
||||
"turn:relay.example.test:3478?transport=tcp&credential=secret",
|
||||
"turn:relay.example.test:99999", "turn:relay example.test:3478",
|
||||
]) assert.equal(isSafeIceUrl(url), false, url);
|
||||
});
|
||||
|
||||
it("rejects extra keys, credential tricks, missing TURN, and excessive lifetime", () => {
|
||||
assert.equal(parseIceConfigRequest({
|
||||
type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1", subject: "stable-user",
|
||||
}).ok, false);
|
||||
assert.equal(parseIceConfigResponse({ ...grant(), identity: { profile: "karti" } }).ok, false);
|
||||
assert.equal(parseIceConfigResponse({ ...grant(), iceServers: [{ urls: ["stun:relay.example.test:3478"] }] }).ok, false);
|
||||
assert.equal(parseIceConfigResponse({ ...grant(), expiresAtMs: 3_601_001 }).ok, false);
|
||||
const turn = grant().iceServers[1] as unknown as Record<string, unknown>;
|
||||
assert.equal(parseIceConfigResponse({
|
||||
...grant(), iceServers: [{ ...turn, urls: ["turn:user@relay.example.test:3478"] }],
|
||||
}).ok, false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { createOfficeScreenPanel, type MediaSurfaceDescriptor } from "../media/index.ts";
|
||||
import { createOfficeScreenPanel } from "../media/officeScreenPanel.ts";
|
||||
import type { MediaSurfaceDescriptor } from "../media/presentation.ts";
|
||||
|
||||
type Listener = (event: FakeEvent) => void;
|
||||
class FakeEvent {
|
||||
@@ -121,6 +122,18 @@ describe("office screen manager panel", () => {
|
||||
assert.equal("mediaDevices" in panel, false);
|
||||
});
|
||||
|
||||
it("keeps presenting separate from viewer opt-in", () => {
|
||||
const { panel, shares, opts } = setup();
|
||||
panel.open();
|
||||
const root = panel.root as unknown as FakeElement;
|
||||
const share = root.find("data-action", "share");
|
||||
assert.equal(share.disabled, false);
|
||||
share.dispatch("click");
|
||||
assert.deepEqual(shares, ["lobby-monitor"]);
|
||||
assert.deepEqual(opts, []);
|
||||
assert.deepEqual(panel.state().optedInScreenIds, []);
|
||||
});
|
||||
|
||||
it("updates defensively, removes stale consent, and represents an empty office", () => {
|
||||
const { panel } = setup();
|
||||
const root = panel.root as unknown as FakeElement;
|
||||
@@ -136,6 +149,39 @@ describe("office screen manager panel", () => {
|
||||
assert.match(root.text(), /No authored office screens/);
|
||||
});
|
||||
|
||||
it("shows concise remote lifecycle states and forgets them with their screen", () => {
|
||||
const { panel, stops, opts } = setup();
|
||||
const root = panel.root as unknown as FakeElement;
|
||||
root.find("data-action", "opt-in").dispatch("click");
|
||||
panel.setRemoteStatus("lobby-monitor", "connecting");
|
||||
assert.equal(panel.state().remoteStatusByScreen["lobby-monitor"], "connecting");
|
||||
assert.match(root.text(), /Remote connecting/);
|
||||
assert.equal(root.find("data-action", "stop").disabled, false);
|
||||
root.find("data-action", "stop").dispatch("click");
|
||||
assert.deepEqual(stops, ["lobby-monitor"]);
|
||||
panel.setRemoteStatus("lobby-monitor", "unavailable");
|
||||
assert.match(root.text(), /No remote share/);
|
||||
assert.equal(root.find("data-action", "opt-in").textContent, "Retry remote");
|
||||
root.find("data-action", "opt-in").dispatch("click");
|
||||
assert.deepEqual(opts.at(-1), ["lobby-monitor", true]);
|
||||
assert.deepEqual(panel.state().optedInScreenIds, ["lobby-monitor"]);
|
||||
panel.update([SURFACES[1]!]);
|
||||
assert.deepEqual(panel.state().remoteStatusByScreen, {});
|
||||
assert.throws(() => panel.setRemoteStatus("commons-display", "invalid" as "live"), /invalid remote status/);
|
||||
});
|
||||
|
||||
it("refreshes a remote surface from bound back to cleared", () => {
|
||||
const { panel } = setup();
|
||||
const root = panel.root as unknown as FakeElement;
|
||||
panel.setRemoteStatus("lobby-monitor", "live");
|
||||
panel.update([{ ...SURFACES[0]!, bound: true }, SURFACES[1]!]);
|
||||
assert.match(root.text(), /Media active · Remote live/);
|
||||
panel.update(SURFACES);
|
||||
panel.setRemoteStatus("lobby-monitor", "off");
|
||||
assert.match(root.text(), /Media off · Remote off/);
|
||||
assert.equal(root.find("data-action", "stop").disabled, true);
|
||||
});
|
||||
|
||||
it("traps tab focus, closes on Escape, restores focus, and disposes idempotently", () => {
|
||||
const { document, container, panel } = setup();
|
||||
const trigger = document.createElement("button");
|
||||
|
||||
@@ -182,6 +182,7 @@ describe("remote office media presenter", () => {
|
||||
/explicit opt-in/,
|
||||
);
|
||||
await media.startPresenter({ consent: { authorized: true, optedIn: true }, stream: stream as unknown as MediaStream, video: video as unknown as HTMLVideoElement });
|
||||
assert.equal(media.state().status, "live", "an empty hosted room is ready for its first viewer");
|
||||
await settle();
|
||||
assert.equal(peers.length, 1);
|
||||
assert.equal(peers[0]?.added[0], presenterTrack as unknown as MediaStreamTrack);
|
||||
@@ -324,7 +325,7 @@ describe("remote office media viewer", () => {
|
||||
});
|
||||
const video = new FakeVideo();
|
||||
await assert.rejects(media.startViewer({ decision: decision(false), video: video as unknown as HTMLVideoElement }), /ready live-stream/);
|
||||
await media.startViewer({ decision: decision(), video: video as unknown as HTMLVideoElement });
|
||||
await media.startViewer({ viewerOptIn: true, video: video as unknown as HTMLVideoElement });
|
||||
await settle();
|
||||
assert.equal(peers[0]?.transceivers[0], "video");
|
||||
assert.deepEqual(peers[0]?.remote[0], { type: "offer", sdp: "presenter-offer" });
|
||||
@@ -340,4 +341,39 @@ describe("remote office media viewer", () => {
|
||||
assert.equal(video.srcObject, null);
|
||||
assert.equal(media.state().status, "revoked");
|
||||
});
|
||||
|
||||
it("forgets its grant when the server has already stopped the session", async () => {
|
||||
const presenterPeer = { participantId: "presenter-opaque", role: "presenter" as const };
|
||||
let emitEvent: (value: Uint8Array) => void = () => { throw new Error("event stream is not open"); };
|
||||
let leaveCalls = 0;
|
||||
const fetcher: typeof fetch = async (input, init = {}) => {
|
||||
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
|
||||
if (String(input).endsWith("/join")) {
|
||||
return Response.json(grant("screen-share-join-grant", String(body.requestId), "viewer", [presenterPeer]), { status: 201 });
|
||||
}
|
||||
if (String(input).endsWith("/events")) {
|
||||
const stream = new ReadableStream<Uint8Array>({ start(controller) { emitEvent = (value) => controller.enqueue(value); } });
|
||||
const resume = grant("screen-share-resume-grant", String(body.requestId), "viewer", [presenterPeer]);
|
||||
emitEvent(new TextEncoder().encode(`data: ${JSON.stringify(resume)}\n\n`));
|
||||
return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream" } });
|
||||
}
|
||||
if (String(input).endsWith("/leave")) leaveCalls += 1;
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
const media = createRemoteOfficeMedia({
|
||||
role: "viewer", binding: BINDING, authenticatedFetch: fetcher, now: () => NOW,
|
||||
peerConnectionFactory: () => new FakePeer() as unknown as RTCPeerConnection,
|
||||
});
|
||||
await media.startViewer({ viewerOptIn: true, video: new FakeVideo() as unknown as HTMLVideoElement });
|
||||
await settle();
|
||||
emitEvent(new TextEncoder().encode(`data: ${JSON.stringify({
|
||||
type: "screen-share-stopped", protocolVersion: 1, sequence: 2, timestampMs: NOW + 1,
|
||||
sessionId: "share-session", binding: BINDING, reason: "presenter-stopped",
|
||||
})}\n\n`));
|
||||
await settle();
|
||||
assert.equal(media.state().status, "stopped");
|
||||
assert.equal(media.state().sessionId, null);
|
||||
await media.dispose();
|
||||
assert.equal(leaveCalls, 0, "cleanup does not reuse a server-invalidated capability");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user