import { videoLoopsOf, videoSpanOf, videoVolumeOf } from '../properties/videos'; import type { VideoAsset, VideoClip } from './Asset'; import { findAsset, type Project } from './Project'; import { clipsOf } from './sound'; import { storedFilePattern } from './storedFile'; const FILE_PATTERN = storedFilePattern('mp4'); export function isVideoFile(name: string): boolean { return FILE_PATTERN.test(name); } /** The still the worker cut from a video (#402), lying beside it. */ export function posterFileOf(file: string): string { return file.replace(/\.mp4$/, '.jpg'); } /** What a texture field names when it names a video: the whole video, or one of its clips. */ export interface VideoRef { asset: VideoAsset; clip: VideoClip | null; } /** A video or a clip of one, by id; clip ids share the generator with asset ids, as frames do. */ export function findVideo(project: Project, id: string): VideoRef | null { if (id === '') return null; const asset = findAsset(project, id); if (asset) return asset.type === 'video' ? { asset, clip: null } : null; for (const video of project.assets) { if (video.type !== 'video') continue; const clip = clipsOf(video).find((c) => c.id === id); if (clip) return { asset: video, clip }; } return null; } /** Every id that means "this video": the asset itself and each of its clips. */ export function videoRefIds(asset: VideoAsset): string[] { return [asset.id, ...clipsOf(asset).map((c) => c.id)]; } /** * The stretch a clip plays, inside the video: a start before its end, an end of 0 or past the * video's length being its end. Without a length (not known yet) the asked values stand. */ export function videoSpan(duration: number, start: number, end: number): { start: number; end: number } { const length = duration > 0 ? duration : Infinity; const from = Math.min(Math.max(0, start), Number.isFinite(length) ? Math.max(0, length - 0.001) : start); const to = end > 0 ? Math.min(end, length) : length; return { start: from, end: to > from ? to : length }; } /** How a video is played: which file, which stretch of it, whether it repeats, how loud. */ export interface VideoPlay { file: string; start: number; /** Seconds; Infinity for the end of the video, wherever that is. */ end: number; loop: boolean; volume: number; } export function videoPlayOf(ref: VideoRef): VideoPlay { const asked = ref.clip ? videoSpanOf(ref.clip.props) : { start: 0, end: 0 }; const span = videoSpan(ref.asset.duration, asked.start, asked.end); return { file: ref.asset.file, start: span.start, end: ref.asset.duration > 0 && span.end >= ref.asset.duration ? Infinity : span.end, loop: videoLoopsOf(ref.asset.props), volume: videoVolumeOf(ref.asset.props), }; } const PREFIX = 'video:'; /** * The name a playing video goes by where a picture is named by `TextureRef.file` (see * `generated.ts`): `video:#~~~`. No uploaded image looks like it, * and one key is one player: two sprites wearing the same clip show the same picture. */ export function videoKey(play: VideoPlay): string { const end = Number.isFinite(play.end) ? play.end : 0; return `${PREFIX}${play.file}#${play.start}~${end}~${play.loop ? 1 : 0}~${play.volume}`; } export function isVideoKey(key: string): boolean { return parseVideoKey(key) !== null; } export function parseVideoKey(key: string): VideoPlay | null { if (!key.startsWith(PREFIX)) return null; const hash = key.indexOf('#'); if (hash < 0) return null; const file = key.slice(PREFIX.length, hash); const parts = key.slice(hash + 1).split('~'); const [start, end, loop, volume] = parts.map(Number); if (!isVideoFile(file) || parts.length !== 4 || ![start, end, loop, volume].every(Number.isFinite)) return null; return { file, start: start!, end: end! > 0 ? end! : Infinity, loop: loop === 1, volume: volume! }; } /** Where in its file a player is after `elapsed` seconds of playing (#402). */ export function videoTimeAt(play: Pick, elapsed: number): number { const length = play.end - play.start; const run = Math.max(0, elapsed); if (!Number.isFinite(length) || length <= 0) return play.start + run; if (!play.loop) return play.start + Math.min(run, length); return play.start + (run % length); }