export const CURRENT_DISPLAY_SPEEDS = Object.freeze([0.5, 1, 1.5, 2]); export function formatPlaybackTime(valueMs) { const milliseconds = Math.max(0, Math.floor(Number(valueMs) || 0)); const minutes = Math.floor(milliseconds / 60000); const seconds = Math.floor((milliseconds % 60000) / 1000); const hundredths = Math.floor((milliseconds % 1000) / 10); return `${minutes}:${String(seconds).padStart(2, "0")}.${String(hundredths).padStart(2, "0")}`; } export function projectPlaybackPosition(playback, elapsedMs = 0) { if (!playback?.active || !Number.isFinite(Number(playback.total_duration_ms))) return null; const total = Math.max(1, Number(playback.total_duration_ms)); const base = Math.max(0, Number(playback.position_ms) || 0); if (playback.paused) return Math.min(total - 1, base); const speed = Number(playback.speed) || 1; const projected = base + Math.max(0, Number(elapsedMs) || 0) * speed; return Math.min(total - 1, projected % total); } export class LatestSeekDispatcher { constructor(send, { intervalMs = 100, now = () => performance.now(), setTimer = (callback, delay) => window.setTimeout(callback, delay), clearTimer = (timer) => window.clearTimeout(timer), onError = () => {}, } = {}) { this.send = send; this.intervalMs = intervalMs; this.now = now; this.setTimer = setTimer; this.clearTimer = clearTimer; this.onError = onError; this.inFlight = false; this.lastStartedAt = Number.NEGATIVE_INFINITY; this.queued = null; this.timer = null; } enqueue(positionMs, { flush = false } = {}) { this.queued = { positionMs, flush: flush || Boolean(this.queued?.flush) }; this.#drain(); } clear() { this.queued = null; if (this.timer !== null) this.clearTimer(this.timer); this.timer = null; } #drain() { if (this.inFlight || this.queued === null) return; const waitMs = this.intervalMs - (this.now() - this.lastStartedAt); if (!this.queued.flush && waitMs > 0) { if (this.timer === null) { this.timer = this.setTimer(() => { this.timer = null; this.#drain(); }, waitMs); } return; } if (this.timer !== null) this.clearTimer(this.timer); this.timer = null; const item = this.queued; this.queued = null; this.inFlight = true; this.lastStartedAt = this.now(); Promise.resolve() .then(() => this.send(item.positionMs)) .catch((error) => this.onError(error, item.positionMs)) .finally(() => { this.inFlight = false; this.#drain(); }); } }