import { CompositionProbe, type ProbeResult } from "./composition-probe.js"; import { isControlsClick, setupControls, setupPoster } from "./controls-setup.js"; import { adoptShadowStyles, createCompositionIframe, scaleIframeToFit } from "./iframe-dom.js"; import { DirectTimelineClock } from "./direct-timeline-clock.js"; import { ParentMediaManager } from "./parent-media.js"; import { handleRuntimeMessage } from "./runtime-message-handler.js"; import { SHADER_CAPTURE_SCALE_ATTR, SHADER_LOADING_ATTR, type ShaderLoadingMode, getShaderCaptureScaleFromElement, getShaderModeFromElement, prepareSrcForElement, prepareSrcdocForElement, } from "./shader-options.js"; import { createShaderLoader } from "./shader-loader-element.js"; import { ShaderLoaderState } from "./shader-loader-state.js"; import { PLAYER_STYLES } from "./styles.js"; import { type DirectTimelineAdapter } from "./timeline-adapters.js"; class HyperframesPlayer extends HTMLElement { static get observedAttributes() { return [ "src", "srcdoc", "width", "height", "controls", "muted", "volume", "poster", "playback-rate", "audio-src", SHADER_CAPTURE_SCALE_ATTR, SHADER_LOADING_ATTR, ]; } private shadow: ShadowRoot; private container: HTMLDivElement; private iframe: HTMLIFrameElement; private posterEl: HTMLImageElement | null = null; private controlsApi: ReturnType | null = null; private resizeObserver: ResizeObserver; private shaderLoader: ShaderLoaderState; private probe: CompositionProbe; private _ready = false; private _currentTime = 0; private _duration = 0; private _paused = true; private _lastUpdateMs = 0; private _volume = 1; private _compositionWidth = 1920; private _compositionHeight = 1080; private _directTimelineAdapter: DirectTimelineAdapter | null = null; private _directTimelineClock: DirectTimelineClock; private _parentTickRaf: number | null = null; private _media: ParentMediaManager; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); adoptShadowStyles(this.shadow, PLAYER_STYLES); ({ container: this.container, iframe: this.iframe } = createCompositionIframe()); this.shadow.appendChild(this.container); const loaderElements = createShaderLoader(); this.shadow.appendChild(loaderElements.root); this.shaderLoader = new ShaderLoaderState(loaderElements); this._media = new ParentMediaManager({ dispatchEvent: (e) => this.dispatchEvent(e), getMuted: () => this.muted, getVolume: () => this._volume, getPlaybackRate: () => this.playbackRate, getCurrentTime: () => this._currentTime, isPaused: () => this._paused, }); this._directTimelineClock = new DirectTimelineClock({ onTimeUpdate: (currentTime, duration) => { this._currentTime = currentTime; this.controlsApi?.updateTime(currentTime, duration); this.dispatchEvent(new CustomEvent("timeupdate", { detail: { currentTime } })); }, getLoop: () => this.loop, restart: () => { this.seek(0); this.play(); }, onPaused: () => { if (this._media.audioOwner === "parent") this._media.pauseAll(); this._paused = true; this.controlsApi?.updatePlaying(false); this.dispatchEvent(new Event("ended")); }, onEnded: () => this.loop, }); this.probe = new CompositionProbe(this.iframe, { onReady: (result) => this._onProbeReady(result), onError: (message) => this.dispatchEvent(new CustomEvent("error", { detail: { message } })), }); this.addEventListener("click", (event) => { if (isControlsClick(event)) return; if (this._paused) this.play(); else this.pause(); }); this.resizeObserver = new ResizeObserver(() => this._rescale()); this._onMessage = this._onMessage.bind(this); this._onIframeLoad = this._onIframeLoad.bind(this); } connectedCallback() { this.resizeObserver.observe(this); window.addEventListener("message", this._onMessage); this.iframe.addEventListener("load", this._onIframeLoad); if (this.hasAttribute("controls")) this._setupControls(); if (this.hasAttribute("poster")) this.posterEl = setupPoster(this.shadow, this.getAttribute("poster"), this.posterEl); if (this.hasAttribute("audio-src")) this._media.setupFromUrl(this.getAttribute("audio-src")!); if (this.hasAttribute("srcdoc")) this.iframe.srcdoc = prepareSrcdocForElement(this, this.getAttribute("srcdoc")!); if (this.hasAttribute("src")) this.iframe.src = prepareSrcForElement(this, this.getAttribute("src")!); } disconnectedCallback() { this.resizeObserver.disconnect(); window.removeEventListener("message", this._onMessage); this.iframe.removeEventListener("load", this._onIframeLoad); this.probe.stop(); this._directTimelineClock.stop(); this._stopParentTickClock(); this._directTimelineAdapter = null; this.shaderLoader.destroy(); this._media.destroy(); this.controlsApi?.destroy(); } attributeChangedCallback(name: string, _old: string | null, val: string | null) { switch (name) { case "src": if (val) { this._ready = false; this.iframe.src = prepareSrcForElement(this, val); } break; case "srcdoc": this._ready = false; if (val !== null) this.iframe.srcdoc = prepareSrcdocForElement(this, val); else this.iframe.removeAttribute("srcdoc"); break; case "width": this._compositionWidth = parseInt(val || "1920", 10); this._rescale(); break; case "height": this._compositionHeight = parseInt(val || "1080", 10); this._rescale(); break; case "controls": if (val !== null) this._setupControls(); else { this.controlsApi?.destroy(); this.controlsApi = null; } break; case "poster": this.posterEl = setupPoster(this.shadow, val, this.posterEl); break; case "playback-rate": { const rate = parseFloat(val || "1"); this._media.updatePlaybackRate(rate); this._sendControl("set-playback-rate", { playbackRate: rate }); this._directTimelineAdapter?.timeScale?.(rate); this.controlsApi?.updateSpeed(rate); this.dispatchEvent(new Event("ratechange")); break; } case "muted": this._media.updateMuted(val !== null); this._sendControl("set-muted", { muted: val !== null }); this.controlsApi?.updateMuted(val !== null); this.dispatchEvent(new Event("volumechange")); break; case "volume": { const v = Math.max(0, Math.min(1, parseFloat(val || "1"))); this._volume = v; this._media.updateVolume(v); this._sendControl("set-volume", { volume: v }); this.controlsApi?.updateVolume(v); this.dispatchEvent(new Event("volumechange")); break; } case "audio-src": if (val) this._media.setupFromUrl(val); break; case SHADER_CAPTURE_SCALE_ATTR: case SHADER_LOADING_ATTR: this._reloadShaderOptions(); break; } } /** * The inner `