1
0

feat: stream private office screens

This commit is contained in:
2026-08-11 21:31:15 -07:00
parent d841575315
commit 5ca214e4bb
28 changed files with 1754 additions and 35 deletions
+31 -9
View File
@@ -60,11 +60,22 @@ export interface PresenterStart {
stream: MediaStream;
video: HTMLVideoElement;
}
export interface ViewerStart {
/** A current `authorizeMediaSurface` decision with explicit viewer opt-in. */
decision: MediaAuthorizationDecision;
video: HTMLVideoElement;
}
export type ViewerStart =
| {
/** A current `authorizeMediaSurface` decision with explicit viewer opt-in. */
decision: MediaAuthorizationDecision;
viewerOptIn?: never;
video: HTMLVideoElement;
}
| {
/**
* Literal user action for signaling servers that perform authorization
* during join and intentionally expose no source locator to the client.
*/
decision?: never;
viewerOptIn: true;
video: HTMLVideoElement;
};
export interface RemoteOfficeMedia {
startPresenter(input: PresenterStart): Promise<void>;
startViewer(input: ViewerStart): Promise<void>;
@@ -310,6 +321,10 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
cursor = advanced.value;
if (message.type === "screen-share-stopped") {
closeTransport();
// The server has already destroyed the signaling session. Forget the
// capability now so caller cleanup cannot issue a redundant leave that
// is guaranteed to fail authorization.
grant = null;
setStatus("stopped");
return;
}
@@ -412,7 +427,10 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
if (!message.continuous) resetPeerMesh();
await reconcile(message.participants);
scheduleRenewal();
setStatus("connecting");
// A presenter with no viewers is still live: the hosted signaling session
// is accepting authorized viewers. A viewer remains connecting until its
// receive-only peer actually reaches the connected state.
setStatus(options.role === "presenter" && peers.size === 0 ? "live" : "connecting");
return true;
}
@@ -492,6 +510,7 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
cursor = { sessionId: response.grant.credential.sessionId, sequence: response.sequence, timestampMs: response.timestampMs };
await reconcile(response.participants);
scheduleRenewal();
if (role === "presenter" && peers.size === 0) setStatus("live");
void startEvents().catch((reason) => { options.onError?.(asError(reason, "remote media events failed")); scheduleReconnect(); });
}
@@ -525,9 +544,12 @@ export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): Remo
async function startViewer(input: ViewerStart): Promise<void> {
if (options.role !== "viewer") throw new Error("remote media: adapter is not a viewer");
if (status === "disposed" || status === "revoked") throw new Error("remote media: session is terminal");
if (!input.decision.authorized || !input.decision.optedIn || !input.decision.canView ||
input.decision.surface.screenId !== binding.screenId || input.decision.surface.officeId !== binding.officeId ||
input.decision.surface.source?.kind !== "live-stream") {
const ready = input.decision === undefined
? input.viewerOptIn === true
: input.decision.authorized && input.decision.optedIn && input.decision.canView &&
input.decision.surface.screenId === binding.screenId && input.decision.surface.officeId === binding.officeId &&
input.decision.surface.source?.kind === "live-stream";
if (!ready) {
throw new Error("remote media: a ready live-stream authorization decision is required");
}
input.video.autoplay = false;