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
+128 -3
View File
@@ -59,6 +59,8 @@ export interface RemoteOfficeMediaOptions {
now?: () => number;
reconnectBaseMs?: number;
reconnectMaximumMs?: number;
/** Bounded sender defaults for every presenter-to-viewer connection. */
presenterEncodingPolicy?: Partial<ScreenShareEncodingPolicy>;
onStateChange?(state: RemoteMediaState): void;
onError?(error: Error): void;
}
@@ -93,10 +95,35 @@ export interface RemoteOfficeMedia {
dispose(): Promise<void>;
}
export interface ScreenShareEncodingPolicy {
maximumWidth: number;
maximumHeight: number;
maximumFrameRate: number;
maximumBitrateBps: number;
contentHint: "detail" | "text";
degradationPreference: RTCDegradationPreference;
}
export interface ScreenShareEncodingPolicyResult {
contentHintApplied: boolean;
senderParametersApplied: boolean;
}
export const DEFAULT_SCREEN_SHARE_ENCODING_POLICY: Readonly<ScreenShareEncodingPolicy> =
Object.freeze({
maximumWidth: 1_280,
maximumHeight: 720,
maximumFrameRate: 15,
maximumBitrateBps: 1_500_000,
contentHint: "detail",
degradationPreference: "maintain-resolution",
});
interface PeerSlot {
readonly id: string;
readonly connection: RTCPeerConnection;
readonly pendingIce: (RTCIceCandidateInit | null)[];
senderPolicyReady: Promise<void>;
}
const ENDPOINTS: RemoteMediaEndpoints = {
join: "/api/v1/media/join",
@@ -136,6 +163,86 @@ function remoteDescription(signal: Extract<ScreenShareSignalPayload, { kind: "sd
return { type: signal.descriptionType, sdp: signal.sdp };
}
function finitePositive(value: number | undefined, fallback: number, name: string): number {
const resolved = value ?? fallback;
if (!Number.isFinite(resolved) || resolved <= 0) {
throw new RangeError(`remote media: ${name} must be finite and positive`);
}
return resolved;
}
function resolveEncodingPolicy(value: Partial<ScreenShareEncodingPolicy> | undefined): ScreenShareEncodingPolicy {
const degradationPreference = value?.degradationPreference ??
DEFAULT_SCREEN_SHARE_ENCODING_POLICY.degradationPreference;
if (
degradationPreference !== "balanced" &&
degradationPreference !== "maintain-framerate" &&
degradationPreference !== "maintain-resolution"
) throw new RangeError("remote media: invalid degradation preference");
const contentHint = value?.contentHint ?? DEFAULT_SCREEN_SHARE_ENCODING_POLICY.contentHint;
if (contentHint !== "detail" && contentHint !== "text") {
throw new RangeError("remote media: invalid screen content hint");
}
return {
maximumWidth: finitePositive(value?.maximumWidth, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumWidth, "maximumWidth"),
maximumHeight: finitePositive(value?.maximumHeight, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumHeight, "maximumHeight"),
maximumFrameRate: finitePositive(value?.maximumFrameRate, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumFrameRate, "maximumFrameRate"),
maximumBitrateBps: finitePositive(value?.maximumBitrateBps, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumBitrateBps, "maximumBitrateBps"),
contentHint,
degradationPreference,
};
}
/** Best-effort WebRTC policy: capability differences never abort signaling. */
export async function applyScreenShareEncodingPolicy(
track: MediaStreamTrack,
sender: RTCRtpSender,
policyValue: Partial<ScreenShareEncodingPolicy> = {},
): Promise<ScreenShareEncodingPolicyResult> {
const policy = resolveEncodingPolicy(policyValue);
let contentHintApplied = false;
try {
track.contentHint = policy.contentHint;
contentHintApplied = track.contentHint === policy.contentHint;
} catch { /* Safari versions without writable contentHint still share safely. */ }
try {
if (typeof sender.getParameters !== "function" || typeof sender.setParameters !== "function") {
return { contentHintApplied, senderParametersApplied: false };
}
const parameters = sender.getParameters();
if (!Array.isArray(parameters.encodings) || parameters.encodings.length === 0) {
return { contentHintApplied, senderParametersApplied: false };
}
let width = 0;
let height = 0;
try {
const settings = track.getSettings?.();
width = typeof settings?.width === "number" ? settings.width : 0;
height = typeof settings?.height === "number" ? settings.height : 0;
} catch { /* Some synthetic/browser tracks do not expose settings. */ }
const scaleResolutionDownBy = Math.max(
1,
width > 0 ? width / policy.maximumWidth : 1,
height > 0 ? height / policy.maximumHeight : 1,
);
for (const encoding of parameters.encodings) {
encoding.maxBitrate = Math.floor(policy.maximumBitrateBps);
encoding.maxFramerate = policy.maximumFrameRate;
encoding.scaleResolutionDownBy = scaleResolutionDownBy;
}
// Older Safari builds omit this dictionary member; do not manufacture it
// because their setParameters implementations may reject unknown fields.
if ("degradationPreference" in parameters) {
parameters.degradationPreference = policy.degradationPreference;
}
await sender.setParameters(parameters);
return { contentHintApplied, senderParametersApplied: true };
} catch {
return { contentHintApplied, senderParametersApplied: false };
}
}
/**
* Capture-free and storage-free by construction. The adapter never invokes a
* media acquisition API, calls `play`, records, uploads media, or exposes the
@@ -153,6 +260,7 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
const now = options.now ?? Date.now;
const reconnectBase = options.reconnectBaseMs ?? 500;
const reconnectMaximum = options.reconnectMaximumMs ?? 15_000;
const presenterEncodingPolicy = resolveEncodingPolicy(options.presenterEncodingPolicy);
const provideIce = options.iceConfigurationProvider ?? ((authorization) =>
fetchEphemeralIceConfiguration({
authenticatedFetch: fetcher,
@@ -172,6 +280,7 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
let cursor: ScreenShareStreamCursor | null = null;
let participants: readonly ScreenShareParticipant[] = [];
let presenter: PresenterStart | null = null;
let presenterOriginalContentHint: string | null = null;
let viewer: ViewerStart | null = null;
let receiverStream: MediaStream | null = null;
let iceConfiguration: EphemeralIceConfiguration | null = null;
@@ -246,6 +355,11 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
releaseReceiver();
iceConfiguration = null;
iceAuthorization = null;
if (presenterOriginalContentHint !== null) {
const track = presenter?.stream.getVideoTracks()[0];
try { if (track) track.contentHint = presenterOriginalContentHint; } catch { /* best effort */ }
presenterOriginalContentHint = null;
}
}
function resetPeerMesh(): void {
@@ -302,7 +416,13 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
throw new Error("remote media: active ICE configuration is required before creating a peer");
}
const connection = makePeer(iceConfiguration.configuration);
const slot: PeerSlot = { id: participant.participantId, connection, pendingIce: [] };
let senderPolicyReady = Promise.resolve();
const slot: PeerSlot = {
id: participant.participantId,
connection,
pendingIce: [],
senderPolicyReady,
};
peers.set(slot.id, slot);
connection.onicecandidate = (event) => {
if (generation !== activeGeneration) return;
@@ -324,7 +444,10 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
if (options.role === "presenter") {
const track = presenter?.stream.getVideoTracks()[0];
if (!track || track.readyState !== "live") throw new Error("remote media: presenter track ended");
connection.addTrack(track, presenter!.stream);
if (presenterOriginalContentHint === null) presenterOriginalContentHint = track.contentHint;
const sender = connection.addTrack(track, presenter!.stream);
senderPolicyReady = applyScreenShareEncodingPolicy(track, sender, presenterEncodingPolicy).then(() => undefined);
slot.senderPolicyReady = senderPolicyReady;
} else {
connection.addTransceiver("video", { direction: "recvonly" });
connection.ontrack = (event) => {
@@ -343,7 +466,9 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
}
async function offer(participant: ScreenShareParticipant): Promise<void> {
const connection = configurePeer(participant).connection;
const slot = configurePeer(participant);
await slot.senderPolicyReady;
const connection = slot.connection;
const description = await connection.createOffer();
await connection.setLocalDescription(description);
await sendSignal(participant.participantId, sdp(connection.localDescription ?? description));