From f820d9a8948b97460ebdfdaa6b42b6d510e2b3cf Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sat, 31 Jan 2026 22:19:11 +0100 Subject: refactor services structure --- apps/web/src/services/media/video-cache.ts | 314 ----------------------------- 1 file changed, 314 deletions(-) delete mode 100644 apps/web/src/services/media/video-cache.ts (limited to 'apps/web/src/services/media') diff --git a/apps/web/src/services/media/video-cache.ts b/apps/web/src/services/media/video-cache.ts deleted file mode 100644 index f3ddfcb2..00000000 --- a/apps/web/src/services/media/video-cache.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { - Input, - ALL_FORMATS, - BlobSource, - CanvasSink, - type WrappedCanvas, -} from "mediabunny"; - -interface VideoSinkData { - sink: CanvasSink; - iterator: AsyncGenerator | null; - currentFrame: WrappedCanvas | null; - nextFrame: WrappedCanvas | null; - lastTime: number; - prefetching: boolean; - prefetchPromise: Promise | null; -} -export class VideoCache { - private sinks = new Map(); - private initPromises = new Map>(); - - async getFrameAt({ - mediaId, - file, - time, - }: { - mediaId: string; - file: File; - time: number; - }): Promise { - await this.ensureSink({ mediaId, file }); - - const sinkData = this.sinks.get(mediaId); - if (!sinkData) return null; - - if (sinkData.nextFrame && sinkData.nextFrame.timestamp <= time) { - sinkData.currentFrame = sinkData.nextFrame; - sinkData.nextFrame = null; - this.startPrefetch({ sinkData }); - } - - if ( - sinkData.currentFrame && - this.isFrameValid({ frame: sinkData.currentFrame, time }) - ) { - if (!sinkData.nextFrame && !sinkData.prefetching) { - this.startPrefetch({ sinkData }); - } - return sinkData.currentFrame; - } - - if ( - sinkData.iterator && - sinkData.currentFrame && - time >= sinkData.lastTime && - time < sinkData.lastTime + 2.0 - ) { - const frame = await this.iterateToTime({ sinkData, targetTime: time }); - if (frame) { - if (!sinkData.nextFrame && !sinkData.prefetching) { - this.startPrefetch({ sinkData }); - } - return frame; - } - } - - const frame = await this.seekToTime({ sinkData, time }); - if (frame && !sinkData.nextFrame && !sinkData.prefetching) { - this.startPrefetch({ sinkData }); - } - return frame; - } - - private isFrameValid({ - frame, - time, - }: { - frame: WrappedCanvas; - time: number; - }): boolean { - return time >= frame.timestamp && time < frame.timestamp + frame.duration; - } - private async iterateToTime({ - sinkData, - targetTime, - }: { - sinkData: VideoSinkData; - targetTime: number; - }): Promise { - if (!sinkData.iterator) return null; - - try { - while (true) { - // Wait for any pending prefetch to finish before touching iterator - if (sinkData.prefetching && sinkData.prefetchPromise) { - await sinkData.prefetchPromise; - } - - // Check if the nextFrame (which might have just arrived) is what we need - if ( - sinkData.nextFrame && - sinkData.nextFrame.timestamp <= targetTime + 0.05 // Tolerance - ) { - sinkData.currentFrame = sinkData.nextFrame; - sinkData.nextFrame = null; - } else { - const { value: frame, done } = await sinkData.iterator.next(); - - if (done || !frame) break; - - sinkData.currentFrame = frame; - } - - const frame = sinkData.currentFrame; - if (!frame) break; - - sinkData.lastTime = frame.timestamp; - - if (this.isFrameValid({ frame, time: targetTime })) { - return frame; - } - - if (frame.timestamp > targetTime + 1.0) break; - } - } catch (error) { - console.warn("Iterator failed, will restart:", error); - sinkData.iterator = null; - } - - return null; - } - private async seekToTime({ - sinkData, - time, - }: { - sinkData: VideoSinkData; - time: number; - }): Promise { - try { - if (sinkData.prefetching && sinkData.prefetchPromise) { - await sinkData.prefetchPromise; - } - - if (sinkData.iterator) { - await sinkData.iterator.return(); - sinkData.iterator = null; - } - - sinkData.nextFrame = null; - sinkData.iterator = sinkData.sink.canvases(time); - sinkData.lastTime = time; - - // Fetch current frame - const { value: frame } = await sinkData.iterator.next(); - - if (frame) { - sinkData.currentFrame = frame; - - // Aggressively fetch next frame immediately to fill buffer - // This matches the mediaplayer example which fetches 2 frames on start - try { - const { value: next } = await sinkData.iterator.next(); - if (next) { - sinkData.nextFrame = next; - } - } catch (e) { - console.warn("Failed to pre-fetch next frame on seek:", e); - } - - return frame; - } - } catch (error) { - console.warn("Failed to seek video:", error); - } - - return null; - } - - private startPrefetch({ sinkData }: { sinkData: VideoSinkData }): void { - if (sinkData.prefetching || !sinkData.iterator || sinkData.nextFrame) { - return; - } - - sinkData.prefetching = true; - sinkData.prefetchPromise = this.prefetchNextFrame({ sinkData }); - } - - private async prefetchNextFrame({ - sinkData, - }: { - sinkData: VideoSinkData; - }): Promise { - if (!sinkData.iterator) { - sinkData.prefetching = false; - sinkData.prefetchPromise = null; - return; - } - - try { - const { value: frame, done } = await sinkData.iterator.next(); - - if (done || !frame) { - sinkData.prefetching = false; - sinkData.prefetchPromise = null; - return; - } - - sinkData.nextFrame = frame; - sinkData.prefetching = false; - sinkData.prefetchPromise = null; - } catch (error) { - console.warn("Prefetch failed:", error); - sinkData.prefetching = false; - sinkData.prefetchPromise = null; - sinkData.iterator = null; - } - } - private async ensureSink({ - mediaId, - file, - }: { - mediaId: string; - file: File; - }): Promise { - if (this.sinks.has(mediaId)) return; - - if (this.initPromises.has(mediaId)) { - await this.initPromises.get(mediaId); - return; - } - - const initPromise = this.initializeSink({ mediaId, file }); - this.initPromises.set(mediaId, initPromise); - - try { - await initPromise; - } finally { - this.initPromises.delete(mediaId); - } - } - private async initializeSink({ - mediaId, - file, - }: { - mediaId: string; - file: File; - }): Promise { - try { - const input = new Input({ - source: new BlobSource(file), - formats: ALL_FORMATS, - }); - - const videoTrack = await input.getPrimaryVideoTrack(); - if (!videoTrack) { - throw new Error("No video track found"); - } - - const canDecode = await videoTrack.canDecode(); - if (!canDecode) { - throw new Error("Video codec not supported for decoding"); - } - - const sink = new CanvasSink(videoTrack, { - poolSize: 3, - fit: "contain", - }); - - this.sinks.set(mediaId, { - sink, - iterator: null, - currentFrame: null, - nextFrame: null, - lastTime: -1, - prefetching: false, - prefetchPromise: null, - }); - } catch (error) { - console.error(`Failed to initialize video sink for ${mediaId}:`, error); - throw error; - } - } - - clearVideo({ mediaId }: { mediaId: string }): void { - const sinkData = this.sinks.get(mediaId); - if (sinkData) { - if (sinkData.iterator) { - void sinkData.iterator.return(); - } - - this.sinks.delete(mediaId); - } - - this.initPromises.delete(mediaId); - } - - clearAll(): void { - for (const [mediaId] of this.sinks) { - this.clearVideo({ mediaId }); - } - } - - getStats() { - return { - totalSinks: this.sinks.size, - activeSinks: Array.from(this.sinks.values()).filter((s) => s.iterator) - .length, - cachedFrames: Array.from(this.sinks.values()).filter( - (s) => s.currentFrame, - ).length, - }; - } -} -export const videoCache = new VideoCache(); -- cgit v1.3.1