UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

117 lines 4.35 kB
import { ALL_FORMATS, CanvasSink, Input, } from "mediabunny"; import { loadDecoderSource } from '../../driver/utils/load-decoder-source.js'; export class Filmstrip { videoTrack; options; #sink; #cache = new Map(); #activeRange = [0, 0]; constructor(videoTrack, options) { this.videoTrack = videoTrack; this.options = options; this.#sink = new CanvasSink(videoTrack, options.canvasSinkOptions); } static async init(source, options) { const input = new Input({ formats: ALL_FORMATS, source: await loadDecoderSource(source) }); const videoTrack = await input.getPrimaryVideoTrack(); if (videoTrack) return new Filmstrip(videoTrack, { frequency: options.frequency ?? 1, canvasSinkOptions: options.canvasSinkOptions ?? { width: 80, height: 50, fit: "fill" }, onChange: options.onChange }); else throw new Error("Source has no video track"); } /** * Sets the frequency (granularity) of filmstrip thumbnails. * Changing this triggers a filmstrip refresh after any ongoing update finishes. * @param value - The new frequency in seconds. */ set frequency(value) { if (value !== this.options.frequency) { this.options.frequency = value; this.#update(); } } get frequency() { return this.options.frequency; } #computeActiveRange([start, end]) { const tileSize = end - start; return [start - tileSize, end + tileSize]; } async #generateTiles() { const [rangeStart, rangeEnd] = this.#activeRange; const neededTimestamps = new Set(); // duration should be computed but with trim etc also const duration = await this.videoTrack.computeDuration(); for (let timestamp = rangeStart; timestamp <= rangeEnd; timestamp += this.options.frequency) { // Clamp to valid time range if (timestamp >= 0 && timestamp <= duration) neededTimestamps.add(+timestamp.toFixed(3)); } const missingTimestamps = [...neededTimestamps] .filter(t => !this.#cache.has(t)); let i = 0; for await (const canvas of this.#sink.canvasesAtTimestamps(missingTimestamps)) { if (canvas) { const requestedTime = missingTimestamps[i++]; this.#cache.set(requestedTime, canvas); } } // Dispose canvases outside the new range for (const key of this.#cache.keys()) { if (!neededTimestamps.has(key)) { this.#cache.delete(key); } } const tiles = [...this.#cache.entries()] .map(([time, canvas]) => ({ time, canvas })); this.options.onChange(tiles); } /** * Updates the visible time range for the filmstrip. * * Triggers a thumbnails update, with extended margins to preload * thumbnails slightly outside the visible range. * @param visibleRange - The current timeline viewport as a [start, end] tuple in seconds. */ set range(visibleRange) { const newRange = this.#computeActiveRange(visibleRange); // Avoid redundant updates if (this.#activeRange[0] === newRange[0] && this.#activeRange[1] === newRange[1]) return; this.#activeRange = newRange; this.#update(); } #updating = null; #shouldRunAgain = false; async #update() { // Perform update immediately. If multiple updates are requested while updating, // only the latest one will run after the current finishes (skips intermediate ones). if (this.#updating) { this.#shouldRunAgain = true; return; } this.#updating = this.#generateTiles(); await this.#updating; this.#updating = null; if (this.#shouldRunAgain) { this.#shouldRunAgain = false; await this.#update(); } } /** * Returns the cached thumbnail (if any) for a given timestamp. * @param time - The timestamp to retrieve the canvas for. */ getThumbnail(time) { return this.#cache.get(time); } } //# sourceMappingURL=filmstrip.js.map