UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

236 lines 9.14 kB
import { ALL_FORMATS, Input, VideoSampleSink } from "mediabunny"; import { ms } from "../../../../units/ms.js"; import { loadDecoderSource } from "../../../../driver/utils/load-decoder-source.js"; import { createVisualSampler } from "../../parts/samplers/visual/sampler.js"; class BaseVisualSampler { driver; resolveMedia; timeline; #videoCursors = new Map(); #sampler; constructor(driver, resolveMedia, timeline) { this.driver = driver; this.resolveMedia = resolveMedia; this.timeline = timeline; this.#sampler = createVisualSampler(this.resolveMedia, (item, time) => { const targetUs = toUs(ms(item.start + time)); let cursor = this.#videoCursors.get(item.id); if (!cursor) { const source = this.resolveMedia(item.mediaHash); const endUs = toUs(ms(item.start + item.duration)); cursor = this.createCursor(source, targetUs, endUs); this.#videoCursors.set(item.id, cursor); } return cursor.next(targetUs); }); } sample(timecode) { return this.#sampler.sample(this.timeline, timecode); } async cancel() { await Promise.all([...this.#videoCursors.values()].map(c => c.cancel())); this.#videoCursors.clear(); } } /** * forward-only frame cursor optimized for export purposes. * it uses mediabunny internally so the support for non-clients * should be done from mediabunny custom decoder/encoder */ export class CursorVisualSampler extends BaseVisualSampler { #lastTimecode = -Infinity; next(timecode) { if (timecode < this.#lastTimecode) throw new Error(`Forward-only cursor regression: ${timecode}ms < ${this.#lastTimecode}ms`); this.#lastTimecode = timecode; return this.sample(timecode); } createCursor(source, startUs, _endUs) { const video = this.driver.decodeVideo({ source, start: startUs / 1_000_000 }); const reader = video.readable.getReader(); let current = null; let nextPromise = null; let ended = false; const readNext = async () => { if (ended) return null; const { done, value } = await reader.read(); if (done) return (ended = true, null); const frame = new VideoFrame(value); value.close(); return frame; }; return { async next(targetUs) { current ??= await readNext(); if (!current) return undefined; while (true) { nextPromise ??= readNext(); const nextFrame = await nextPromise; if (!nextFrame) return new VideoFrame(current); const currentUs = current.timestamp ?? -Infinity; const nextUs = nextFrame.timestamp ?? currentUs; if (nextUs < targetUs) { current.close(); current = nextFrame; nextPromise = null; continue; } const useNext = Math.abs(nextUs - targetUs) < Math.abs(currentUs - targetUs); if (useNext) { current.close(); current = nextFrame; nextPromise = null; continue; } return new VideoFrame(current); } }, async cancel() { const pending = nextPromise; nextPromise = null; ended = true; const buffered = await pending?.catch(() => null); buffered?.close(); current?.close(); current = null; video.cancel(); } }; } } export class ReverseCursorVisualSampler extends BaseVisualSampler { #lastTimecode = Infinity; next(timecode) { if (timecode > this.#lastTimecode) throw new Error(`Reverse-only cursor regression: ${timecode}ms > ${this.#lastTimecode}ms`); this.#lastTimecode = timecode; return this.sample(timecode); } createCursor(source, startUs, endUs) { const windowUs = 1_000_000; const prefetchThreshold = windowUs * 0.5; let frames = []; let windowStart = Infinity; let windowEnd = -Infinity; let input = null; let sink = null; let prefetchPromise = null; let activeFetches = 0; let idle = Promise.resolve(); let resolveIdle = null; let canceled = false; const clear = () => { for (const frame of frames) frame.close(); frames = []; }; const startFetch = () => { if (activeFetches++ === 0) idle = new Promise(resolve => resolveIdle = resolve); }; const endFetch = () => { if (--activeFetches === 0) { resolveIdle?.(); resolveIdle = null; } }; const getSink = async () => { if (sink) return sink; input = new Input({ source: await loadDecoderSource(source), formats: ALL_FORMATS, }); const track = await input.getPrimaryVideoTrack(); sink = track && await track.canDecode() ? new VideoSampleSink(track) : null; return sink; }; const fetchFrames = async (targetUs) => { startFetch(); const wEnd = Math.min(endUs, targetUs + 1); const wStart = Math.max(startUs, wEnd - windowUs); const newFrames = []; const videoSink = await getSink(); if (videoSink) { for await (const sample of videoSink.samples(wStart / 1_000_000, wEnd / 1_000_000)) { newFrames.push(sample.toVideoFrame()); sample.close(); } } endFetch(); return { frames: newFrames, windowStart: wStart, windowEnd: wEnd }; }; const loadWindow = async (targetUs) => { clear(); const result = await fetchFrames(targetUs); frames = result.frames; windowStart = result.windowStart; windowEnd = result.windowEnd; }; return { async next(targetUs) { if (canceled) return undefined; if (targetUs < windowStart || targetUs > windowEnd) { if (prefetchPromise) { const prefetched = await prefetchPromise; prefetchPromise = null; if (canceled) { for (const f of prefetched.frames) f.close(); return undefined; } if (targetUs >= prefetched.windowStart && targetUs <= prefetched.windowEnd) { clear(); frames = prefetched.frames; windowStart = prefetched.windowStart; windowEnd = prefetched.windowEnd; } else { for (const f of prefetched.frames) f.close(); await loadWindow(targetUs); } } else { await loadWindow(targetUs); } } if (!prefetchPromise && targetUs < windowStart + prefetchThreshold && windowStart > startUs) prefetchPromise = fetchFrames(windowStart - 1); let best; let bestDistance = Infinity; for (const frame of frames) { const distance = Math.abs((frame.timestamp ?? targetUs) - targetUs); if (distance < bestDistance) { best = frame; bestDistance = distance; } } return best ? new VideoFrame(best) : undefined; }, async cancel() { canceled = true; const pending = prefetchPromise; prefetchPromise = null; const prefetched = await pending?.catch(() => null); if (prefetched) for (const f of prefetched.frames) f.close(); await idle; clear(); input?.dispose(); input = null; sink = null; } }; } } const toUs = (ms) => Math.round(ms * 1_000); //# sourceMappingURL=cursor.js.map