import type { RuntimePlayer, RuntimeTimelineLike } from "./types"; import { quantizeTimeToFrame } from "../inline-scripts/parityContract"; type PlayerDeps = { getTimeline: () => RuntimeTimelineLike | null; setTimeline: (timeline: RuntimeTimelineLike | null) => void; getIsPlaying: () => boolean; setIsPlaying: (playing: boolean) => void; getPlaybackRate: () => number; setPlaybackRate: (rate: number) => void; getCanonicalFps: () => number; onSyncMedia: (timeSeconds: number, playing: boolean) => void; onStatePost: (force: boolean) => void; onDeterministicSeek: (timeSeconds: number) => void; onDeterministicPause: () => void; onDeterministicPlay: () => void; onRenderFrameSeek: (timeSeconds: number) => void; onShowNativeVideos: () => void; getSafeDuration?: () => number; }; function seekTimelineDeterministically( timeline: RuntimeTimelineLike, timeSeconds: number, canonicalFps: number, ): number { const quantized = quantizeTimeToFrame(timeSeconds, canonicalFps); timeline.pause(); if (typeof timeline.totalTime === "function") { timeline.totalTime(quantized, false); } else { timeline.seek(quantized, false); } return quantized; } export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer { return { _timeline: null, play: () => { const timeline = deps.getTimeline(); if (!timeline || deps.getIsPlaying()) return; const safeDuration = Math.max( 0, Number(deps.getSafeDuration?.() ?? timeline.duration() ?? 0) || 0, ); if (safeDuration > 0) { const currentTime = Math.max(0, Number(timeline.time()) || 0); if (currentTime >= safeDuration) { timeline.pause(); timeline.seek(0, false); deps.onDeterministicSeek(0); deps.setIsPlaying(false); deps.onSyncMedia(0, false); deps.onRenderFrameSeek(0); } } if (typeof timeline.timeScale === "function") { timeline.timeScale(deps.getPlaybackRate()); } timeline.play(); deps.onDeterministicPlay(); deps.setIsPlaying(true); deps.onShowNativeVideos(); deps.onStatePost(true); }, pause: () => { const timeline = deps.getTimeline(); if (!timeline) return; timeline.pause(); const time = Math.max(0, Number(timeline.time()) || 0); deps.onDeterministicSeek(time); deps.onDeterministicPause(); deps.setIsPlaying(false); deps.onSyncMedia(time, false); deps.onRenderFrameSeek(time); deps.onStatePost(true); }, seek: (timeSeconds: number) => { const timeline = deps.getTimeline(); if (!timeline) return; const safeTime = Math.max(0, Number(timeSeconds) || 0); const quantized = seekTimelineDeterministically(timeline, safeTime, deps.getCanonicalFps()); deps.onDeterministicSeek(quantized); deps.setIsPlaying(false); deps.onSyncMedia(quantized, false); deps.onRenderFrameSeek(quantized); deps.onStatePost(true); }, renderSeek: (timeSeconds: number) => { const timeline = deps.getTimeline(); if (!timeline) return; const quantized = seekTimelineDeterministically( timeline, timeSeconds, deps.getCanonicalFps(), ); deps.onDeterministicSeek(quantized); deps.setIsPlaying(false); deps.onSyncMedia(quantized, false); deps.onRenderFrameSeek(quantized); deps.onStatePost(true); }, getTime: () => Number(deps.getTimeline()?.time() ?? 0), getDuration: () => Number(deps.getTimeline()?.duration() ?? 0), isPlaying: () => deps.getIsPlaying(), setPlaybackRate: (rate: number) => deps.setPlaybackRate(rate), getPlaybackRate: () => deps.getPlaybackRate(), }; }