mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-07 07:09:15 +00:00
* feat(player): play and loop a range of the film with range-start and range-end The runtime takes a set-play-range control and stops on the range end frame itself. The player wraps to range-start with loop, fires ended without it, and reports a range cut to the film with rangeclamped. Video and __timelines players check it per tick. * fix(player): hold a range on its last frame and let the runtime own range jumps A runtime with the play-range capability jumps to range-start itself when a new range leaves the playhead outside, so a stale state no longer ends the film; older runtimes keep the player fallback, guarded until a state lands inside. Every range stop shows the last frame before range-end; a paused playhead re-parks only when it falls outside the new range. * fix(player): stop a range a check early and enforce its end on a video in a background tab * test(player): share the range video setup * fix(player): a slow frame no longer cuts a range short; a video without a range is unchanged * fix(player): a range still stops early where video time moves once per frame * refactor(player): cut the range look-ahead comment to one line * fix(player): a per-frame video time rounded down still stops a range early * test(player): pin no rounding tolerance at a film's own end * test(studio): expect the play-range capability on control messages
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
// Video mode: a `<video playsinline>` behind the direct-timeline adapter a same-origin
|
|
// `__timelines` composition uses, so play, seek, loop and `ended` share one code path.
|
|
|
|
import type { DirectTimelineAdapter } from "./timeline-adapters.js";
|
|
|
|
export function isVideoType(type: string | null): boolean {
|
|
return type?.trim().toLowerCase().startsWith("video/") ?? false;
|
|
}
|
|
|
|
export interface VideoSourceCallbacks {
|
|
onMetadata: (video: HTMLVideoElement) => void;
|
|
onDurationChange: (video: HTMLVideoElement) => void;
|
|
onResize: (video: HTMLVideoElement) => void;
|
|
onPlay: (video: HTMLVideoElement) => void;
|
|
onPause: (video: HTMLVideoElement) => void;
|
|
onTimeUpdate: () => void;
|
|
onError: (message: string, code: number | null) => void;
|
|
onPlayRejected: (error: unknown) => void;
|
|
}
|
|
|
|
export interface VideoSource {
|
|
video: HTMLVideoElement;
|
|
adapter: DirectTimelineAdapter;
|
|
destroy: () => void;
|
|
}
|
|
|
|
export function createVideoSource(callbacks: VideoSourceCallbacks): VideoSource {
|
|
const video = document.createElement("video");
|
|
video.className = "hfp-video";
|
|
video.playsInline = true;
|
|
video.preload = "metadata";
|
|
|
|
const listening = new AbortController();
|
|
const { signal } = listening;
|
|
video.addEventListener("loadedmetadata", () => callbacks.onMetadata(video), { signal });
|
|
video.addEventListener("durationchange", () => callbacks.onDurationChange(video), { signal });
|
|
video.addEventListener("resize", () => callbacks.onResize(video), { signal });
|
|
video.addEventListener("play", () => callbacks.onPlay(video), { signal });
|
|
video.addEventListener("pause", () => callbacks.onPause(video), { signal });
|
|
video.addEventListener("timeupdate", () => callbacks.onTimeUpdate(), { signal });
|
|
video.addEventListener(
|
|
"error",
|
|
() =>
|
|
callbacks.onError(video.error?.message || "Video failed to load", video.error?.code ?? null),
|
|
{ signal },
|
|
);
|
|
|
|
const adapter: DirectTimelineAdapter = {
|
|
duration: () => video.duration,
|
|
time: () => video.currentTime,
|
|
seek: (timeInSeconds) => {
|
|
video.currentTime = timeInSeconds;
|
|
},
|
|
// A rejection that lands after destroy() belongs to a source that is gone.
|
|
play: () =>
|
|
video.play().catch((error: unknown) => {
|
|
if (!signal.aborted) callbacks.onPlayRejected(error);
|
|
}),
|
|
pause: () => video.pause(),
|
|
// The default rate survives a new src; the current rate alone would reset to it.
|
|
timeScale: (rate) => {
|
|
video.defaultPlaybackRate = rate;
|
|
video.playbackRate = rate;
|
|
},
|
|
};
|
|
|
|
return {
|
|
video,
|
|
adapter,
|
|
destroy: () => {
|
|
listening.abort();
|
|
video.pause();
|
|
video.removeAttribute("src");
|
|
video.load();
|
|
video.remove();
|
|
},
|
|
};
|
|
}
|