1
0

feat: add ephemeral webcam faces and adaptive screens

This commit is contained in:
2026-08-11 22:38:56 -07:00
parent 3326d2e6d0
commit d4859b33c6
17 changed files with 1107 additions and 11 deletions
+36
View File
@@ -170,4 +170,40 @@ describe("city and office actor acceptance handoff", () => {
walker.dispose();
cityActor.dispose();
});
it("moves one caller-owned live face across the office door and appearance rebuilds", () => {
const identity: ActorIdentity = {
id: "member-camera",
displayName: "Camera Member",
authenticated: true,
profile: { appearance: { primaryColor: "#18222d", accentColor: "#55b8c8" } },
};
const cityActor = createSceneActor({ kind: "humanoid", identity });
const walker = createOfficeWalker(officePlan(), {
levelId: "ground",
position: { x: 2, z: 2 },
actor: { kind: "humanoid", outfitColor: "#18222d", accentColor: "#55b8c8" },
});
const texture = new THREE.Texture();
let disposals = 0;
texture.addEventListener("dispose", () => { disposals += 1; });
assert.equal(cityActor.attachFaceTexture(texture), true);
assert.equal((cityActor.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);
cityActor.clearFaceTexture();
assert.equal(walker.attachFaceTexture(texture), true);
walker.setAppearance({ kind: "humanoid", outfitColor: "#53616d", accentColor: "#db6d62" });
assert.equal((walker.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);
walker.clearFaceTexture();
assert.equal(cityActor.attachFaceTexture(texture), true);
cityActor.setIdentity({
...identity,
profile: { appearance: { primaryColor: "#53616d", accentColor: "#db6d62" } },
});
assert.equal((cityActor.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);
walker.dispose();
cityActor.dispose();
assert.equal(disposals, 0, "actor adapters never dispose the app-owned live texture");
});
});
+33
View File
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, it } from "node:test";
const staticRunbook = readFileSync(
fileURLToPath(new URL("../../deploy/STATIC.md", import.meta.url)),
"utf8",
);
describe("production media permissions policy", () => {
const policy = staticRunbook.match(/Permissions-Policy\s+"([^"]+)"/)?.[1] ?? "";
it("allows only same-origin contextual camera and display prompts", () => {
assert.match(policy, /(?:^|,\s*)display-capture=\(self\)(?:,|$)/);
assert.match(policy, /(?:^|,\s*)camera=\(self\)(?:,|$)/);
assert.doesNotMatch(policy, /camera=\(\*\)/);
assert.doesNotMatch(policy, /display-capture=\(\*\)/);
});
it("keeps audio and unrelated high-risk capabilities denied", () => {
for (const capability of ["microphone", "geolocation", "payment", "usb"]) {
assert.match(policy, new RegExp(`(?:^|,\\s*)${capability}=\\(\\)(?:,|$)`));
}
});
it("documents denial, indicator, stop, and page lifecycle acceptance", () => {
assert.match(staticRunbook, /denying the browser prompt/i);
assert.match(staticRunbook, /active indicator/i);
assert.match(staticRunbook, /stop on the in-product control or `pagehide`/i);
assert.match(staticRunbook, /neither webcam faces nor office screens request audio/i);
});
});
+22
View File
@@ -225,6 +225,28 @@ describe("profile editor DOM adapter", () => {
assert.equal(saves.length, 1);
});
it("includes visible contextual controls in its focus trap and skips hidden ones", () => {
const { document, editor } = setup();
const cameraStart = document.createElement("button");
const cameraStop = document.createElement("button");
cameraStop.hidden = true;
editor.registerFocusables([
cameraStart as unknown as HTMLElement,
cameraStop as unknown as HTMLElement,
]);
editor.open();
const root = editor.root as unknown as FakeElement;
const first = root.find("data-field", "displayName");
cameraStart.focus();
assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
assert.equal(document.activeElement, first);
cameraStart.hidden = true;
const save = root.find("data-action", "save");
save.focus();
assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
assert.equal(document.activeElement, first);
});
it("strictly updates caller data, then disposes without leaving DOM", () => {
const { container, editor, previews } = setup();
const next = createDefaultLocalProfile("new-person", "Morgan");
+135 -1
View File
@@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
DEFAULT_SCREEN_SHARE_ENCODING_POLICY,
applyScreenShareEncodingPolicy,
createRemoteOfficeMedia,
type MediaAuthorizationDecision,
type RemoteMediaTimers,
@@ -21,9 +23,40 @@ class FakeTrack {
readonly kind = "video";
readyState: MediaStreamTrackState = "live";
stopped = 0;
contentHint = "";
private readonly settings: MediaTrackSettings;
constructor(settings: MediaTrackSettings = { width: 3_840, height: 2_160, frameRate: 60 }) {
this.settings = settings;
}
getSettings(): MediaTrackSettings { return { ...this.settings }; }
stop(): void { this.stopped += 1; this.readyState = "ended"; }
}
class FakeSender {
readonly writes: RTCRtpSendParameters[] = [];
private readonly rejectWrites: boolean;
private readonly supported: boolean;
constructor(rejectWrites = false, supported = true) {
this.rejectWrites = rejectWrites;
this.supported = supported;
}
getParameters(): RTCRtpSendParameters {
if (!this.supported) throw new Error("getParameters unsupported");
return {
encodings: [{}],
degradationPreference: "balanced",
headerExtensions: [],
codecs: [],
rtcp: { cname: "fake", reducedSize: false },
transactionId: "fake",
};
}
async setParameters(value: RTCRtpSendParameters): Promise<void> {
if (this.rejectWrites) throw new DOMException("unsupported", "NotSupportedError");
this.writes.push(structuredClone(value));
}
}
class FakeStream {
readonly tracks: FakeTrack[];
constructor(tracks: FakeTrack[]) { this.tracks = tracks; }
@@ -48,9 +81,15 @@ class FakePeer {
readonly remote: RTCSessionDescriptionInit[] = [];
readonly ice: (RTCIceCandidateInit | null)[] = [];
readonly transceivers: string[] = [];
readonly senders: FakeSender[] = [];
closed = 0;
addTrack(track: MediaStreamTrack): RTCRtpSender { this.added.push(track); return {} as RTCRtpSender; }
addTrack(track: MediaStreamTrack): RTCRtpSender {
this.added.push(track);
const sender = new FakeSender();
this.senders.push(sender);
return sender as unknown as RTCRtpSender;
}
addTransceiver(kind: string): RTCRtpTransceiver { this.transceivers.push(kind); return {} as RTCRtpTransceiver; }
async createOffer(): Promise<RTCSessionDescriptionInit> { return { type: "offer", sdp: "presenter-offer" }; }
async createAnswer(): Promise<RTCSessionDescriptionInit> { return { type: "answer", sdp: "viewer-answer" }; }
@@ -176,6 +215,41 @@ async function settle(): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
describe("screen share sender policy", () => {
it("caps a 4K sender at the explicit 720p/15 bandwidth contract", async () => {
const track = new FakeTrack();
const sender = new FakeSender();
const result = await applyScreenShareEncodingPolicy(
track as unknown as MediaStreamTrack,
sender as unknown as RTCRtpSender,
);
assert.deepEqual(result, { contentHintApplied: true, senderParametersApplied: true });
assert.equal(track.contentHint, "detail");
assert.equal(DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumWidth, 1_280);
assert.equal(DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumHeight, 720);
assert.deepEqual(sender.writes[0]?.encodings, [{
maxBitrate: 1_500_000,
maxFramerate: 15,
scaleResolutionDownBy: 3,
}]);
assert.equal(sender.writes[0]?.degradationPreference, "maintain-resolution");
});
it("falls back without throwing when a browser rejects sender parameters", async () => {
const track = new FakeTrack();
const rejected = await applyScreenShareEncodingPolicy(
track as unknown as MediaStreamTrack,
new FakeSender(true) as unknown as RTCRtpSender,
);
assert.deepEqual(rejected, { contentHintApplied: true, senderParametersApplied: false });
const unsupported = await applyScreenShareEncodingPolicy(
track as unknown as MediaStreamTrack,
{} as RTCRtpSender,
);
assert.deepEqual(unsupported, { contentHintApplied: true, senderParametersApplied: false });
});
});
describe("remote office media presenter", () => {
it("requires opt-in, publishes one caller track, and keeps credentials in authenticated POST bodies", async () => {
const presenterTrack = new FakeTrack();
@@ -210,6 +284,7 @@ describe("remote office media presenter", () => {
await settle();
assert.equal(peers.length, 1);
assert.equal(peers[0]?.added[0], presenterTrack as unknown as MediaStreamTrack);
assert.equal(peers[0]?.senders[0]?.writes[0]?.encodings[0]?.maxFramerate, 15);
assert.equal(video.autoplay, false);
assert.equal(video.muted, true);
const signal = calls.find((call) => call.url.endsWith("/signal"));
@@ -229,9 +304,68 @@ describe("remote office media presenter", () => {
assert.equal(JSON.stringify(media.state()).includes("private-presenter-grant"), false);
await media.stop();
assert.equal(presenterTrack.stopped, 0, "caller-owned capture track survives stop");
assert.equal(presenterTrack.contentHint, "", "caller-owned track metadata is restored on stop");
assert.equal(peers[0]?.closed, 1);
});
it("applies the bounded sender policy independently to late-joining viewers", async () => {
const first = { participantId: "viewer-first", role: "viewer" as const };
const late = { participantId: "viewer-late", role: "viewer" as const };
const peers: FakePeer[] = [];
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
const ice = iceResponse(input, body);
if (ice) return ice;
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", [first]), { status: 201 });
}
if (String(input).endsWith("/events")) {
return sse([
grant("screen-share-resume-grant", String(body.requestId), "presenter", [first]),
{
type: "screen-share-participants",
protocolVersion: 1,
sequence: 2,
timestampMs: NOW + 1,
sessionId: "share-session",
binding: BINDING,
participants: [first, late],
leaseExpiresAtMs: NOW + 60_000,
},
]);
}
return new Response(null, { status: 204 });
};
const track = new FakeTrack();
const media = createRemoteOfficeMedia({
role: "presenter",
binding: BINDING,
authenticatedFetch: fetcher,
now: () => NOW,
peerConnectionFactory: () => {
const peer = new FakePeer();
peers.push(peer);
return peer as unknown as RTCPeerConnection;
},
});
await media.startPresenter({
consent: { authorized: true, optedIn: true },
stream: new FakeStream([track]) as unknown as MediaStream,
video: new FakeVideo() as unknown as HTMLVideoElement,
});
await settle();
assert.equal(peers.length, 2);
for (const peer of peers) {
assert.equal(peer.added[0], track as unknown as MediaStreamTrack);
assert.equal(peer.senders[0]?.writes.length, 1);
assert.equal(peer.senders[0]?.writes[0]?.encodings[0]?.maxBitrate, 1_500_000);
assert.equal(peer.senders[0]?.writes[0]?.encodings[0]?.scaleResolutionDownBy, 3);
}
await media.stop();
assert.equal(track.stopped, 0);
assert.equal(track.contentHint, "");
});
it("tears down failed peers and rebuilds them from a resumed participant snapshot", async () => {
const timers = new FakeTimers();
const peers: FakePeer[] = [];
+94
View File
@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createWebcamCapture } from "../profile/index.ts";
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
}
class FakeTrack {
stops = 0;
stop(): void { this.stops += 1; }
}
class FakeStream {
readonly tracks: FakeTrack[];
constructor(tracks: FakeTrack[]) { this.tracks = tracks; }
getTracks(): FakeTrack[] { return this.tracks; }
}
class FakeVideo {
srcObject: MediaStream | null = null;
pauses = 0;
removals = 0;
readonly playing: Promise<void>;
constructor(playing: Promise<void> = Promise.resolve()) { this.playing = playing; }
play(): Promise<void> { return this.playing; }
pause(): void { this.pauses += 1; }
remove(): void { this.removals += 1; }
}
describe("ephemeral webcam capture ownership", () => {
it("stops a permission result that arrives after explicit Stop", async () => {
const acquired = deferred<MediaStream>();
const track = new FakeTrack();
let videos = 0;
const capture = createWebcamCapture({
acquire: () => acquired.promise,
createVideo: () => { videos += 1; return new FakeVideo() as unknown as HTMLVideoElement; },
});
const start = capture.start();
assert.equal(capture.status(), "requesting");
capture.stop();
acquired.resolve(new FakeStream([track]) as unknown as MediaStream);
assert.equal(await start, null);
assert.equal(track.stops, 1);
assert.equal(videos, 0, "cancelled permission never creates a video surface");
assert.equal(capture.binding(), null);
});
it("owns the stream before play settles so pagehide-style Stop is immediate", async () => {
const playing = deferred<void>();
const tracks = [new FakeTrack(), new FakeTrack()];
const video = new FakeVideo(playing.promise);
const capture = createWebcamCapture({
acquire: async () => new FakeStream(tracks) as unknown as MediaStream,
createVideo: () => video as unknown as HTMLVideoElement,
});
const start = capture.start();
await Promise.resolve();
assert.ok(capture.binding(), "pending playback is already caller-owned");
capture.stop();
assert.deepEqual(tracks.map((track) => track.stops), [1, 1]);
assert.equal(video.pauses, 1);
assert.equal(video.srcObject, null);
assert.equal(video.removals, 1);
playing.resolve();
assert.equal(await start, null);
assert.equal(capture.status(), "off");
});
it("releases every active track on Stop and dispose without retaining media", async () => {
const tracks = [new FakeTrack(), new FakeTrack()];
const video = new FakeVideo();
const stream = new FakeStream(tracks) as unknown as MediaStream;
const capture = createWebcamCapture({
acquire: async () => stream,
createVideo: () => video as unknown as HTMLVideoElement,
});
const binding = await capture.start();
assert.equal(binding?.stream, stream);
assert.equal(capture.status(), "active");
const exposed = capture.binding();
assert.notEqual(exposed, binding, "binding snapshots do not expose mutable controller state");
capture.dispose();
capture.dispose();
assert.deepEqual(tracks.map((track) => track.stops), [1, 1]);
assert.equal(capture.binding(), null);
assert.equal(capture.status(), "disposed");
await assert.rejects(capture.start(), /disposed/);
});
});
+145
View File
@@ -0,0 +1,145 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createWebcamFacePanel } from "../profile/index.ts";
type Listener = () => void;
class FakeElement {
readonly children: FakeElement[] = [];
readonly attributes = new Map<string, string>();
readonly listeners = new Map<string, Listener[]>();
parentElement: FakeElement | null = null;
className = "";
textContent = "";
hidden = false;
disabled = false;
type = "";
readonly ownerDocument: FakeDocument;
readonly tagName: string;
constructor(ownerDocument: FakeDocument, tagName: string) {
this.ownerDocument = ownerDocument;
this.tagName = tagName.toUpperCase();
}
append(...nodes: FakeElement[]): void {
for (const node of nodes) {
node.parentElement = this;
this.children.push(node);
}
}
setAttribute(name: string, value: string): void { this.attributes.set(name, value); }
getAttribute(name: string): string | null { return this.attributes.get(name) ?? null; }
addEventListener(type: string, listener: Listener): void {
const listeners = this.listeners.get(type) ?? [];
listeners.push(listener);
this.listeners.set(type, listeners);
}
dispatch(type: string): void { for (const listener of this.listeners.get(type) ?? []) listener(); }
focus(): void { this.ownerDocument.activeElement = this; }
remove(): void {
if (!this.parentElement) return;
const index = this.parentElement.children.indexOf(this);
if (index >= 0) this.parentElement.children.splice(index, 1);
this.parentElement = null;
}
find(attribute: string, value: string): FakeElement {
if (this.attributes.get(attribute) === value) return this;
for (const child of this.children) {
try { return child.find(attribute, value); } catch { /* keep looking */ }
}
throw new Error(`missing [${attribute}=${value}]`);
}
descendants(tagName: string): FakeElement[] {
return this.children.flatMap((child) => [
...(child.tagName === tagName.toUpperCase() ? [child] : []),
...child.descendants(tagName),
]);
}
}
class FakeDocument {
activeElement: FakeElement | null = null;
createElement(tagName: string): FakeElement { return new FakeElement(this, tagName); }
}
function setup(supported = true) {
const document = new FakeDocument();
const container = document.createElement("div");
const indicatorContainer = document.createElement("div");
let starts = 0;
let stops = 0;
const panel = createWebcamFacePanel({
container: container as unknown as HTMLElement,
indicatorContainer: indicatorContainer as unknown as HTMLElement,
supported,
onStart: () => { starts += 1; },
onStop: () => { stops += 1; },
});
return { container, indicatorContainer, panel, starts: () => starts, stops: () => stops };
}
describe("webcam face controls", () => {
it("starts off explicitly off and emits capture intent only from the Start button", () => {
const { panel, starts, stops, indicatorContainer } = setup();
const root = panel.root as unknown as FakeElement;
const start = root.find("data-action", "start-camera");
const stop = root.find("data-action", "stop-camera");
assert.deepEqual(panel.state(), {
status: "off",
message: "Camera is off. Nothing is captured or stored.",
indicatorVisible: false,
});
assert.equal((panel.indicator as unknown as FakeElement).hidden, true);
assert.equal(indicatorContainer.children.length, 1);
stop.dispatch("click");
assert.equal(stops(), 0);
start.dispatch("click");
assert.equal(starts(), 1);
assert.equal(stops(), 0);
});
it("keeps a visible active indicator with one-click Stop outside the dialog", () => {
const { panel, stops } = setup();
const root = panel.root as unknown as FakeElement;
const start = root.find("data-action", "start-camera");
const stop = root.find("data-action", "stop-camera");
start.focus();
panel.update("active");
assert.equal(panel.state().indicatorVisible, true);
assert.equal(root.getAttribute("data-status"), "active");
const indicator = panel.indicator as unknown as FakeElement;
assert.equal(indicator.hidden, false);
assert.equal(indicator.getAttribute("role"), "region");
assert.equal(indicator.getAttribute("aria-label"), "Camera active");
assert.equal(indicator.descendants("span")[1]?.getAttribute("role"), "status");
assert.equal(start.hidden, true);
assert.equal(stop.hidden, false);
assert.equal(start.ownerDocument.activeElement, stop, "focus follows the asynchronous state change");
indicator.find("data-action", "stop-camera-indicator").dispatch("click");
assert.equal(stops(), 1);
});
it("handles requesting, denial text, unsupported browsers, and disposal defensively", () => {
const first = setup();
const start = (first.panel.root as unknown as FakeElement).find("data-action", "start-camera");
first.panel.update("requesting");
assert.equal(start.disabled, true);
start.dispatch("click");
assert.equal(first.starts(), 0);
const denial = "Camera permission was denied. Nothing was captured.";
first.panel.update("error", denial);
assert.equal(first.panel.state().message, denial);
const unsupported = setup(false);
const unsupportedStart = (unsupported.panel.root as unknown as FakeElement).find("data-action", "start-camera");
assert.equal(unsupported.panel.state().status, "unsupported");
assert.equal(unsupportedStart.disabled, true);
unsupportedStart.dispatch("click");
assert.equal(unsupported.starts(), 0);
assert.throws(() => first.panel.update("bogus" as never), RangeError);
first.panel.dispose();
first.panel.dispose();
assert.equal(first.container.children.length, 0);
assert.equal(first.indicatorContainer.children.length, 0);
});
});