63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|
|
|