import type { VideoTexture } from "three"; import type { MediaAuthorizationDecision } from "./types.ts"; export interface MediaSurfaceBinding { /** All objects are caller-owned. This adapter never obtains or fetches them. */ video: HTMLVideoElement; texture: VideoTexture; track?: MediaStreamTrack; } export interface MediaSurfaceLifecycle { bind(binding: MediaSurfaceBinding): void; apply(decision: MediaAuthorizationDecision): void; texture(): VideoTexture | null; optedIn(): boolean; dispose(): void; } /** * Gate an already-created video texture behind a server decision and explicit * viewer consent. `play`, `getUserMedia`, `fetch`, `track.stop`, and * `texture.dispose` are intentionally absent: acquisition and ownership remain * with the caller. */ export function createMediaSurfaceLifecycle(): MediaSurfaceLifecycle { let binding: MediaSurfaceBinding | null = null; let allowed = false; let consent = false; let disposed = false; function silence(video: HTMLVideoElement): void { video.autoplay = false; video.muted = true; video.pause(); } return { bind(next) { if (disposed) throw new Error("media surface lifecycle is disposed"); if (binding) silence(binding.video); binding = next; silence(next.video); }, apply(decision) { if (disposed) return; allowed = decision.canView && decision.surface.source !== null; consent = decision.optedIn; if (!allowed && binding) silence(binding.video); }, texture: () => (!disposed && allowed && consent ? binding?.texture ?? null : null), optedIn: () => consent, dispose() { if (disposed) return; disposed = true; if (binding) silence(binding.video); binding = null; allowed = false; consent = false; }, }; }