UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

133 lines 4.88 kB
import { renderTile } from "./parts/render.js"; import { collectPeakLevels } from "./parts/collect.js"; const MAX_TILE_WIDTH = 4096; export class Waveform { #tiles = new Map(); #activeRange = [0, 0]; #visibleRange = [0, 0]; #zoom; #levels; #onChange; #updateQueued = false; color; duration; tileSize; tileHeight; preloadMargin; constructor(levels, duration, options) { this.#levels = levels; this.duration = duration; this.tileSize = options.tileSize ?? 1; this.#zoom = options.zoom ?? ((options.tileWidth ?? 256) / this.tileSize); this.tileHeight = options.tileHeight ?? 96; this.preloadMargin = options.preloadMargin ?? 2; this.color = options.color ?? "rgb(3, 148, 129)"; this.#onChange = options.onChange; } static async init(driver, source, options = {}) { const { duration, levels } = await collectPeakLevels(driver, source); return new Waveform(levels, duration, options); } /** Waveform render density in pixels per second. */ set zoom(pixelsPerSecond) { const next = Math.max(1, pixelsPerSecond); if (next === this.#zoom) return; this.#zoom = next; this.#tiles.clear(); this.#queueUpdate(); } get zoom() { return this.#zoom; } get range() { return this.#visibleRange; } #computeActiveRange([start, end], margin = 1) { const visibleSize = end - start; return [ Math.max(0, start - visibleSize * margin), Math.min(this.duration, end + visibleSize * margin), ]; } set range(visibleRange) { this.#visibleRange = visibleRange; const [visibleStart, visibleEnd] = visibleRange; const visibleSize = visibleEnd - visibleStart; const [activeStart, activeEnd] = this.#activeRange; const leftBuffered = activeStart > 0; const rightBuffered = activeEnd < this.duration; const leftSettled = !leftBuffered || visibleStart >= activeStart + visibleSize; const rightSettled = !rightBuffered || visibleEnd <= activeEnd - visibleSize; if (leftSettled && rightSettled) return; this.#activeRange = this.#computeActiveRange(visibleRange, this.preloadMargin); this.#queueUpdate(); } #queueUpdate() { if (this.#updateQueued) return; this.#updateQueued = true; queueMicrotask(() => { this.#updateQueued = false; this.#generateTiles(); }); } #generateTiles() { const [rangeStart, rangeEnd] = this.#activeRange; const neededStarts = new Set(); const level = this.#levelForZoom(); const firstStart = Math.max(0, Math.floor(rangeStart / this.tileSize) * this.tileSize); const lastStart = Math.min(this.duration, rangeEnd); for (let startTime = firstStart; startTime <= lastStart; startTime += this.tileSize) { neededStarts.add(startTime); } for (const startTime of neededStarts) { if (!this.#tiles.has(startTime)) { const endTime = Math.min(startTime + this.tileSize, this.duration); this.#tiles.set(startTime, this.#buildTileData(startTime, endTime, level)); } } for (const startTime of this.#tiles.keys()) { if (!neededStarts.has(startTime)) this.#tiles.delete(startTime); } this.#emit(); } #buildTileData(startTime, endTime, level) { const peaks = this.#slicePeaks(level, startTime, endTime); return { startTime, endTime, peaks, canvas: renderTile(peaks, { width: this.#tilePixelWidth(startTime, endTime), height: this.tileHeight, color: this.color, }), }; } #levelForZoom() { return this.#levels.find(level => level.peaksPerSecond >= this.#zoom) ?? this.#levels[this.#levels.length - 1]; } #slicePeaks(level, startTime, endTime) { if (!level.peaksPerSecond) return new Float32Array(); const from = Math.max(0, Math.floor(startTime * level.peaksPerSecond)); const to = Math.max(from + 1, Math.min(level.peaks.length, Math.ceil(endTime * level.peaksPerSecond))); return level.peaks.slice(from, to); } #tilePixelWidth(startTime, endTime) { return Math.min(MAX_TILE_WIDTH, Math.max(1, Math.ceil((endTime - startTime) * this.#zoom))); } #emit() { if (!this.#onChange) return; this.#onChange([...this.#tiles.values()].sort((a, b) => a.startTime - b.startTime)); } getTiles() { return this.#tiles; } } //# sourceMappingURL=waveform.js.map