summaryrefslogtreecommitdiff
path: root/apps/web/src/services/video-cache
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
commit56ca09692a308b22455420d11376ff8d41244c47 (patch)
tree4eca5ae032451c5db954801008aa9f164b2e2b1d /apps/web/src/services/video-cache
parente6c6193638e4247ff1e6f2df40de2a057112c773 (diff)
downloadopencut-56ca09692a308b22455420d11376ff8d41244c47.tar.gz
refactor: thinner hooks
Diffstat (limited to 'apps/web/src/services/video-cache')
-rw-r--r--apps/web/src/services/video-cache/service.ts45
1 files changed, 25 insertions, 20 deletions
diff --git a/apps/web/src/services/video-cache/service.ts b/apps/web/src/services/video-cache/service.ts
index e5f3991d..cad4fc31 100644
--- a/apps/web/src/services/video-cache/service.ts
+++ b/apps/web/src/services/video-cache/service.ts
@@ -7,6 +7,7 @@ import {
} from "mediabunny";
interface VideoSinkData {
+ input: Input;
sink: CanvasSink;
iterator: AsyncGenerator<WrappedCanvas, void, unknown> | null;
currentFrame: WrappedCanvas | null;
@@ -20,6 +21,7 @@ export class VideoCache {
private sinks = new Map<string, VideoSinkData>();
private initPromises = new Map<string, Promise<void>>();
private frameChain = new Map<string, Promise<unknown>>();
+ private seekGenerations = new Map<string, number>();
async getFrameAt({
mediaId,
@@ -35,11 +37,20 @@ export class VideoCache {
const sinkData = this.sinks.get(mediaId);
if (!sinkData) return null;
+ const generation = (this.seekGenerations.get(mediaId) ?? 0) + 1;
+ this.seekGenerations.set(mediaId, generation);
+
const previous = this.frameChain.get(mediaId) ?? Promise.resolve();
- const current = previous.then(() =>
- this.resolveFrame({ sinkData, time }),
+ const current = previous.then(() => {
+ if (this.seekGenerations.get(mediaId) !== generation) {
+ return sinkData.currentFrame ?? null;
+ }
+ return this.resolveFrame({ sinkData, time });
+ });
+ this.frameChain.set(
+ mediaId,
+ current.catch(() => {}),
);
- this.frameChain.set(mediaId, current.catch(() => {}));
return current;
}
@@ -172,18 +183,7 @@ export class VideoCache {
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);
- }
-
+ this.startPrefetch({ sinkData });
return frame;
}
} catch (error) {
@@ -262,12 +262,12 @@ export class VideoCache {
mediaId: string;
file: File;
}): Promise<void> {
- try {
- const input = new Input({
- source: new BlobSource(file),
- formats: ALL_FORMATS,
- });
+ const input = new Input({
+ source: new BlobSource(file),
+ formats: ALL_FORMATS,
+ });
+ try {
const videoTrack = await input.getPrimaryVideoTrack();
if (!videoTrack) {
throw new Error("No video track found");
@@ -284,6 +284,7 @@ export class VideoCache {
});
this.sinks.set(mediaId, {
+ input,
sink,
iterator: null,
currentFrame: null,
@@ -293,6 +294,7 @@ export class VideoCache {
prefetchPromise: null,
});
} catch (error) {
+ input.dispose();
console.error(`Failed to initialize video sink for ${mediaId}:`, error);
throw error;
}
@@ -305,10 +307,13 @@ export class VideoCache {
void sinkData.iterator.return();
}
+ sinkData.input.dispose();
this.sinks.delete(mediaId);
}
this.initPromises.delete(mediaId);
+ this.frameChain.delete(mediaId);
+ this.seekGenerations.delete(mediaId);
}
clearAll(): void {