@meframe/core 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/controllers/PlaybackController.d.ts +6 -0
  2. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  3. package/dist/controllers/PlaybackController.js +66 -17
  4. package/dist/controllers/PlaybackController.js.map +1 -1
  5. package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
  6. package/dist/controllers/PlaybackStateMachine.js +1 -3
  7. package/dist/controllers/PlaybackStateMachine.js.map +1 -1
  8. package/dist/controllers/types.d.ts +1 -4
  9. package/dist/controllers/types.d.ts.map +1 -1
  10. package/dist/controllers/types.js +0 -1
  11. package/dist/controllers/types.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/orchestrator/OnDemandVideoSession.d.ts +8 -8
  16. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  17. package/dist/orchestrator/OnDemandVideoSession.js +112 -39
  18. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  19. package/dist/orchestrator/Orchestrator.d.ts +0 -5
  20. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  21. package/dist/orchestrator/Orchestrator.js +15 -43
  22. package/dist/orchestrator/Orchestrator.js.map +1 -1
  23. package/dist/orchestrator/types.d.ts +6 -1
  24. package/dist/orchestrator/types.d.ts.map +1 -1
  25. package/dist/stages/mux/MP4Muxer.d.ts +1 -0
  26. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  27. package/dist/stages/mux/MP4Muxer.js +7 -2
  28. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  29. package/dist/utils/video-decoder-helpers.d.ts +28 -0
  30. package/dist/utils/video-decoder-helpers.d.ts.map +1 -1
  31. package/dist/utils/video-decoder-helpers.js +123 -0
  32. package/dist/utils/video-decoder-helpers.js.map +1 -1
  33. package/package.json +1 -1
@@ -17,8 +17,6 @@ interface OnDemandVideoSessionConfig {
17
17
  fps: number;
18
18
  }
19
19
  /**
20
- * OnDemandVideoSession - Main-thread on-demand decoder
21
- *
22
20
  * Strategy:
23
21
  * 1. Read GOP range from OPFS
24
22
  * 2. Demux using MP4Box (main thread)
@@ -46,7 +44,6 @@ export declare class OnDemandVideoSession {
46
44
  private readonly fps;
47
45
  private readonly globalTimeUs;
48
46
  private readonly targetTimeUs;
49
- private decoder;
50
47
  isDisposed: boolean;
51
48
  private aborted;
52
49
  private decodedFrames;
@@ -57,6 +54,12 @@ export declare class OnDemandVideoSession {
57
54
  * Decode a window range, write raw frames to L1 cache
58
55
  */
59
56
  decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void>;
57
+ /**
58
+ * Scrub decode (timeline dragging):
59
+ * Decode only the minimum frames needed to present a true frame near targetTimeUs.
60
+ * This avoids the heavy 3s window decode used for playback preheating.
61
+ */
62
+ decodeScrub(targetTimeUs: TimeUs): Promise<void>;
60
63
  /**
61
64
  * Handle image resource by creating a VideoFrame from ImageBitmap
62
65
  */
@@ -65,6 +68,8 @@ export declare class OnDemandVideoSession {
65
68
  * Handle video resource by decoding from OPFS
66
69
  */
67
70
  private handleVideoResource;
71
+ private handleVideoResourceScrub;
72
+ private buildEncodedChunksFromSamples;
68
73
  /**
69
74
  * Release all decoded frames without caching
70
75
  */
@@ -84,11 +89,6 @@ export declare class OnDemandVideoSession {
84
89
  */
85
90
  private cacheFrame;
86
91
  private cacheDecodedFrames;
87
- /**
88
- * Fast decode single keyframe for immediate seek preview
89
- * Returns the decoded keyframe timestamp
90
- */
91
- decodeKeyframe(targetTimeUs: TimeUs): Promise<TimeUs | null>;
92
92
  /**
93
93
  * Decode entire time range to VideoFrame stream (for export)
94
94
  * Does NOT cache to L1 - outputs frames directly for Worker pipeline
@@ -1 +1 @@
1
- {"version":3,"file":"OnDemandVideoSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAO,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAUpE,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,oBAAoB;IAC/B;;;OAGG;WACU,wBAAwB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EAAE,EAC3B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAoChB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,OAAO,CAA6B;IAC5C,UAAU,UAAS;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO;WAYM,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAMxE,IAAI;IAIlB;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BjE;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,mBAAmB;IAqCjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,2BAA2B;IA8CnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;IAsB1B;;OAEG;IACH,OAAO,CAAC,UAAU;YAcJ,kBAAkB;IA8BhC;;;OAGG;IACG,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAwClE;;;OAGG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YA2FhF,6BAA6B;IAkBrC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAoB/B"}
1
+ {"version":3,"file":"OnDemandVideoSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAUpE,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAoB;IAC/B;;;OAGG;WACU,wBAAwB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EAAE,EAC3B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAoChB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,UAAU,UAAS;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO;WAYM,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAMxE,IAAI;IAIlB;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BjE;;;;OAIG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtD;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,mBAAmB;YAqCnB,wBAAwB;IAiFtC,OAAO,CAAC,6BAA6B;IAiCrC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,2BAA2B;IA8CnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;IAsB1B;;OAEG;IACH,OAAO,CAAC,UAAU;YAcJ,kBAAkB;IA8BhC;;;OAGG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YA2FhF,6BAA6B;IAkBrC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAU/B"}
@@ -1,5 +1,5 @@
1
- import { binarySearchOverlapping } from "../utils/binary-search.js";
2
- import { decodeChunksWithoutFlush } from "../utils/video-decoder-helpers.js";
1
+ import { binarySearchRange, binarySearchOverlapping } from "../utils/binary-search.js";
2
+ import { decodeChunksWithoutFlush, decodeChunksForScrub } from "../utils/video-decoder-helpers.js";
3
3
  import { ResourceCorruptedError } from "../utils/errors.js";
4
4
  class OnDemandVideoSession {
5
5
  /**
@@ -44,7 +44,6 @@ class OnDemandVideoSession {
44
44
  fps;
45
45
  globalTimeUs;
46
46
  targetTimeUs;
47
- decoder = null;
48
47
  isDisposed = false;
49
48
  aborted = false;
50
49
  decodedFrames = [];
@@ -96,6 +95,26 @@ class OnDemandVideoSession {
96
95
  }
97
96
  await this.handleVideoResource(startUs, endUs);
98
97
  }
98
+ /**
99
+ * Scrub decode (timeline dragging):
100
+ * Decode only the minimum frames needed to present a true frame near targetTimeUs.
101
+ * This avoids the heavy 3s window decode used for playback preheating.
102
+ */
103
+ async decodeScrub(targetTimeUs) {
104
+ if (this.isDisposed) {
105
+ throw new Error("Session already disposed");
106
+ }
107
+ const resource = this.compositionModel.getResource(this.resourceId);
108
+ if (!resource) {
109
+ throw new Error(`Resource not found: ${this.resourceId}`);
110
+ }
111
+ if (resource.type === "image") {
112
+ const frameDuration = Math.max(1, Math.round(1e6 / this.fps));
113
+ await this.handleImageResource(resource, targetTimeUs, targetTimeUs + frameDuration);
114
+ return;
115
+ }
116
+ await this.handleVideoResourceScrub(targetTimeUs);
117
+ }
99
118
  /**
100
119
  * Handle image resource by creating a VideoFrame from ImageBitmap
101
120
  */
@@ -140,6 +159,96 @@ class OnDemandVideoSession {
140
159
  }
141
160
  await this.cacheDecodedFrames(startUs, endUs);
142
161
  }
162
+ async handleVideoResourceScrub(targetTimeUs) {
163
+ const index = this.mp4IndexCache.get(this.resourceId);
164
+ if (!index?.tracks.video) {
165
+ throw new Error(`No video index found for resource ${this.resourceId}`);
166
+ }
167
+ const videoTrack = index.tracks.video;
168
+ const { gopIndex, samples } = videoTrack;
169
+ const gop = binarySearchRange(gopIndex, targetTimeUs, (g, idx) => {
170
+ const next = gopIndex[idx + 1];
171
+ return { start: g.startTimeUs, end: next ? next.startTimeUs : Infinity };
172
+ });
173
+ if (!gop) {
174
+ const frameDuration = Math.max(1, Math.round(1e6 / this.fps));
175
+ await this.handleVideoResource(targetTimeUs, targetTimeUs + frameDuration);
176
+ return;
177
+ }
178
+ const startIdx = gop.keyframeSampleIndex;
179
+ const endIdx = Math.min(samples.length, startIdx + gop.sampleCount);
180
+ if (startIdx >= endIdx) return;
181
+ let byteStart = Infinity;
182
+ let byteEnd = 0;
183
+ for (let i = startIdx; i < endIdx; i++) {
184
+ const s = samples[i];
185
+ if (!s) continue;
186
+ byteStart = Math.min(byteStart, s.byteOffset);
187
+ byteEnd = Math.max(byteEnd, s.byteOffset + s.byteLength);
188
+ }
189
+ if (!Number.isFinite(byteStart) || byteEnd <= byteStart) return;
190
+ const gopData = await this.readResourceRangeWithRecovery(byteStart, byteEnd);
191
+ if (this.aborted) return;
192
+ const chunks = this.buildEncodedChunksFromSamples(
193
+ gopData,
194
+ byteStart,
195
+ samples,
196
+ startIdx,
197
+ endIdx
198
+ );
199
+ if (chunks.length === 0) return;
200
+ const timeoutMs = this.cacheManager.isExporting ? 15e3 : 2e3;
201
+ const { before, after } = await decodeChunksForScrub(
202
+ chunks,
203
+ {
204
+ codec: videoTrack.codec,
205
+ width: videoTrack.width,
206
+ height: videoTrack.height,
207
+ description: videoTrack.description
208
+ },
209
+ targetTimeUs,
210
+ {
211
+ timeoutMs,
212
+ maxQueueSize: 2,
213
+ shouldAbort: () => this.aborted
214
+ }
215
+ );
216
+ if (this.aborted) {
217
+ before?.close();
218
+ after?.close();
219
+ return;
220
+ }
221
+ for (const frame of [before, after]) {
222
+ if (!frame) continue;
223
+ try {
224
+ this.cacheFrame(frame);
225
+ } catch {
226
+ frame.close();
227
+ }
228
+ }
229
+ }
230
+ buildEncodedChunksFromSamples(data, baseByteOffset, samples, startIdx, endIdx) {
231
+ const chunks = [];
232
+ const dataView = new Uint8Array(data);
233
+ for (let i = startIdx; i < endIdx; i++) {
234
+ const sample = samples[i];
235
+ if (!sample) continue;
236
+ const relativeOffset = sample.byteOffset - baseByteOffset;
237
+ if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {
238
+ continue;
239
+ }
240
+ const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);
241
+ chunks.push(
242
+ new EncodedVideoChunk({
243
+ type: sample.isKeyframe ? "key" : "delta",
244
+ timestamp: sample.timestamp,
245
+ duration: sample.duration,
246
+ data: sampleData
247
+ })
248
+ );
249
+ }
250
+ return chunks;
251
+ }
143
252
  /**
144
253
  * Release all decoded frames without caching
145
254
  */
@@ -278,35 +387,6 @@ class OnDemandVideoSession {
278
387
  }
279
388
  this.decodedFrames = [];
280
389
  }
281
- /**
282
- * Fast decode single keyframe for immediate seek preview
283
- * Returns the decoded keyframe timestamp
284
- */
285
- async decodeKeyframe(targetTimeUs) {
286
- if (this.isDisposed || this.aborted) return null;
287
- const index = this.mp4IndexCache.get(this.resourceId);
288
- if (!index || !index.tracks.video) return null;
289
- const keyframeSample = this.mp4IndexCache.getNearestKeyframe(this.resourceId, targetTimeUs);
290
- if (!keyframeSample) return null;
291
- const keyframeData = await this.readResourceRangeWithRecovery(
292
- keyframeSample.byteOffset,
293
- keyframeSample.byteOffset + keyframeSample.byteLength
294
- );
295
- if (this.aborted) return null;
296
- const chunk = new EncodedVideoChunk({
297
- type: "key",
298
- timestamp: keyframeSample.timestamp,
299
- duration: keyframeSample.duration,
300
- data: keyframeData
301
- });
302
- await this.decodeChunks([chunk], index);
303
- if (this.aborted || this.decodedFrames.length === 0) return null;
304
- const frame = this.decodedFrames[0];
305
- if (!frame) return null;
306
- this.cacheFrame(frame);
307
- this.decodedFrames = [];
308
- return keyframeSample.timestamp;
309
- }
310
390
  /**
311
391
  * Decode entire time range to VideoFrame stream (for export)
312
392
  * Does NOT cache to L1 - outputs frames directly for Worker pipeline
@@ -395,13 +475,6 @@ class OnDemandVideoSession {
395
475
  async dispose() {
396
476
  if (this.isDisposed) return;
397
477
  this.aborted = true;
398
- if (this.decoder) {
399
- try {
400
- this.decoder.close();
401
- } catch {
402
- }
403
- this.decoder = null;
404
- }
405
478
  this.releaseDecodedFrames();
406
479
  this.isDisposed = true;
407
480
  }
@@ -1 +1 @@
1
- {"version":3,"file":"OnDemandVideoSession.js","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP } from '../stages/demux/types';\nimport type { TimeUs, Resource } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksWithoutFlush } from '../utils/video-decoder-helpers';\nimport { ResourceCorruptedError } from '../utils/errors';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface OnDemandVideoSessionConfig {\n clipId: string;\n resourceId: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * OnDemandVideoSession - Main-thread on-demand decoder\n *\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class OnDemandVideoSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n private decoder: VideoDecoder | null = null;\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n\n private constructor(config: OnDemandVideoSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession> {\n const session = new OnDemandVideoSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n console.warn('[OnDemandVideoSession] Resource not found in composition model:', {\n resourceId: this.resourceId,\n clipId: this.clipId,\n startUs,\n endUs,\n model: {\n fps: this.compositionModel.fps,\n durationUs: this.compositionModel.durationUs,\n mainTrackId: this.compositionModel.mainTrackId,\n trackCount: this.compositionModel.tracks.length,\n resourcesSize: this.compositionModel.resources.size,\n },\n });\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) return;\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) return;\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[OnDemandVideoSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : undefined;\n const result = await decodeChunksWithoutFlush(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n timeoutMs ? { timeoutMs } : undefined\n );\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Fast decode single keyframe for immediate seek preview\n * Returns the decoded keyframe timestamp\n */\n async decodeKeyframe(targetTimeUs: TimeUs): Promise<TimeUs | null> {\n if (this.isDisposed || this.aborted) return null;\n\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index || !index.tracks.video) return null;\n\n // Find nearest keyframe\n const keyframeSample = this.mp4IndexCache.getNearestKeyframe(this.resourceId, targetTimeUs);\n if (!keyframeSample) return null;\n\n // Read only the keyframe bytes from OPFS\n const keyframeData = await this.readResourceRangeWithRecovery(\n keyframeSample.byteOffset,\n keyframeSample.byteOffset + keyframeSample.byteLength\n );\n\n if (this.aborted) return null;\n\n // Create and decode keyframe chunk\n const chunk = new EncodedVideoChunk({\n type: 'key',\n timestamp: keyframeSample.timestamp,\n duration: keyframeSample.duration,\n data: keyframeData,\n });\n\n await this.decodeChunks([chunk], index);\n\n if (this.aborted || this.decodedFrames.length === 0) return null;\n\n // Cache the first (and only) decoded frame\n const frame = this.decodedFrames[0];\n if (!frame) return null;\n\n this.cacheFrame(frame);\n this.decodedFrames = [];\n\n return keyframeSample.timestamp;\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[OnDemandVideoSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(batchByteStart, batchByteEnd);\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue decoded frames (DO NOT cache to L1)\n for (const frame of this.decodedFrames) {\n controller.enqueue(frame);\n }\n\n // Clear frames array (ownership transferred to stream)\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[OnDemandVideoSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n private async readResourceRangeWithRecovery(start: number, end: number): Promise<ArrayBuffer> {\n try {\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n } catch (error) {\n if (!(error instanceof ResourceCorruptedError)) throw error;\n\n // Unify recovery behavior:\n // 1) Invalidate OPFS + index (ensure cache consistency)\n // 2) Reload to OPFS + rebuild index\n // 3) Retry the read once\n await this.cacheManager.resourceCache.deleteResource(this.resourceId);\n this.cacheManager.mp4IndexCache.delete(this.resourceId);\n\n await this.resourceLoader.load(this.resourceId, { isPreload: false, clipId: this.clipId });\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n }\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Clean up decoder if exists\n if (this.decoder) {\n try {\n this.decoder.close();\n } catch {\n // Ignore close errors during dispose\n }\n this.decoder = null;\n }\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OnDemandVideoSession.js","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP, Sample } from '../stages/demux/types';\nimport type { TimeUs, Resource } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping, binarySearchRange } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksForScrub, decodeChunksWithoutFlush } from '../utils/video-decoder-helpers';\nimport { ResourceCorruptedError } from '../utils/errors';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface OnDemandVideoSessionConfig {\n clipId: string;\n resourceId: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class OnDemandVideoSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n\n private constructor(config: OnDemandVideoSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession> {\n const session = new OnDemandVideoSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n console.warn('[OnDemandVideoSession] Resource not found in composition model:', {\n resourceId: this.resourceId,\n clipId: this.clipId,\n startUs,\n endUs,\n model: {\n fps: this.compositionModel.fps,\n durationUs: this.compositionModel.durationUs,\n mainTrackId: this.compositionModel.mainTrackId,\n trackCount: this.compositionModel.tracks.length,\n resourcesSize: this.compositionModel.resources.size,\n },\n });\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Scrub decode (timeline dragging):\n * Decode only the minimum frames needed to present a true frame near targetTimeUs.\n * This avoids the heavy 3s window decode used for playback preheating.\n */\n async decodeScrub(targetTimeUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleImageResource(resource, targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n await this.handleVideoResourceScrub(targetTimeUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) return;\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) return;\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n private async handleVideoResourceScrub(targetTimeUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`No video index found for resource ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const { gopIndex, samples } = videoTrack;\n\n const gop = binarySearchRange(gopIndex, targetTimeUs, (g, idx) => {\n const next = gopIndex[idx + 1];\n return { start: g.startTimeUs, end: next ? next.startTimeUs : Infinity };\n });\n if (!gop) {\n // Fallback: use a tiny window decode.\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleVideoResource(targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = Math.min(samples.length, startIdx + gop.sampleCount);\n if (startIdx >= endIdx) return;\n\n // Compute byte range for the GOP (sample offsets are not guaranteed monotonic).\n let byteStart = Infinity;\n let byteEnd = 0;\n for (let i = startIdx; i < endIdx; i++) {\n const s = samples[i];\n if (!s) continue;\n byteStart = Math.min(byteStart, s.byteOffset);\n byteEnd = Math.max(byteEnd, s.byteOffset + s.byteLength);\n }\n if (!Number.isFinite(byteStart) || byteEnd <= byteStart) return;\n\n const gopData = await this.readResourceRangeWithRecovery(byteStart, byteEnd);\n if (this.aborted) return;\n\n const chunks = this.buildEncodedChunksFromSamples(\n gopData,\n byteStart,\n samples,\n startIdx,\n endIdx\n );\n if (chunks.length === 0) return;\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : 2_000;\n const { before, after } = await decodeChunksForScrub(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n targetTimeUs,\n {\n timeoutMs,\n maxQueueSize: 2,\n shouldAbort: () => this.aborted,\n }\n );\n\n if (this.aborted) {\n before?.close();\n after?.close();\n return;\n }\n\n // Cache only the closest frames around target.\n for (const frame of [before, after]) {\n if (!frame) continue;\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n }\n\n private buildEncodedChunksFromSamples(\n data: ArrayBuffer,\n baseByteOffset: number,\n samples: Sample[],\n startIdx: number,\n endIdx: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - baseByteOffset;\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n continue;\n }\n\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n chunks.push(\n new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n })\n );\n }\n\n return chunks;\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[OnDemandVideoSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : undefined;\n const result = await decodeChunksWithoutFlush(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n timeoutMs ? { timeoutMs } : undefined\n );\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[OnDemandVideoSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(batchByteStart, batchByteEnd);\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue decoded frames (DO NOT cache to L1)\n for (const frame of this.decodedFrames) {\n controller.enqueue(frame);\n }\n\n // Clear frames array (ownership transferred to stream)\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[OnDemandVideoSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n private async readResourceRangeWithRecovery(start: number, end: number): Promise<ArrayBuffer> {\n try {\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n } catch (error) {\n if (!(error instanceof ResourceCorruptedError)) throw error;\n\n // Unify recovery behavior:\n // 1) Invalidate OPFS + index (ensure cache consistency)\n // 2) Reload to OPFS + rebuild index\n // 3) Retry the read once\n await this.cacheManager.resourceCache.deleteResource(this.resourceId);\n this.cacheManager.mp4IndexCache.delete(this.resourceId);\n\n await this.resourceLoader.load(this.resourceId, { isPreload: false, clipId: this.clipId });\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n }\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":";;;AAyCO,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,aAAa,yBACX,aACA,QACA,OACA,MACA,cACA,KACe;AACf,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,WAAY;AAGjB,UAAM,aAAa,OAAO,CAAC;AAC3B,QAAI,CAAC,cAAc,WAAW,SAAS,OAAO;AAC5C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,yBAAyB,QAAQ;AAAA,QACpD,OAAO,WAAW;AAAA,QAClB,OAAO,WAAW;AAAA,QAClB,QAAQ,WAAW;AAAA,QACnB,aAAa,WAAW;AAAA,MAAA,CACzB;AAGD,YAAM,gBAAgB,KAAK,MAAM,MAAY,GAAG;AAChD,iBAAW,SAAS,OAAO,QAAQ;AACjC,cAAM,kBAAkB,KAAK,UAAU,MAAM;AAC7C,qBAAa;AAAA,UACX;AAAA,UACA,KAAK;AAAA,UACL;AAAA,UACA,KAAK,WAAW;AAAA,UAChB;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EACiB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,aAAa;AAAA,EACL,UAAU;AAAA,EACV,gBAA8B,CAAA;AAAA,EAE9B,YAAY,QAAoC;AACtD,SAAK,SAAS,OAAO;AACrB,SAAK,aAAa,OAAO;AACzB,SAAK,gBAAgB,OAAO;AAC5B,SAAK,eAAe,OAAO;AAC3B,SAAK,iBAAiB,OAAO;AAC7B,SAAK,mBAAmB,OAAO;AAC/B,SAAK,MAAM,OAAO;AAClB,SAAK,eAAe,OAAO;AAC3B,SAAK,eAAe,OAAO;AAAA,EAC7B;AAAA,EAEA,aAAa,OAAO,QAAmE;AACrF,UAAM,UAAU,IAAI,qBAAqB,MAAM;AAC/C,UAAM,QAAQ,KAAA;AACd,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,OAAsB;AAAA,EAEpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAAiB,OAA8B;AAChE,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAEA,UAAM,WAAW,KAAK,iBAAiB,YAAY,KAAK,UAAU;AAClE,QAAI,CAAC,UAAU;AACb,cAAQ,KAAK,mEAAmE;AAAA,QAC9E,YAAY,KAAK;AAAA,QACjB,QAAQ,KAAK;AAAA,QACb;AAAA,QACA;AAAA,QACA,OAAO;AAAA,UACL,KAAK,KAAK,iBAAiB;AAAA,UAC3B,YAAY,KAAK,iBAAiB;AAAA,UAClC,aAAa,KAAK,iBAAiB;AAAA,UACnC,YAAY,KAAK,iBAAiB,OAAO;AAAA,UACzC,eAAe,KAAK,iBAAiB,UAAU;AAAA,QAAA;AAAA,MACjD,CACD;AACD,YAAM,IAAI,MAAM,uBAAuB,KAAK,UAAU,EAAE;AAAA,IAC1D;AAEA,QAAI,SAAS,SAAS,SAAS;AAC7B,YAAM,KAAK,oBAAoB,UAAU,SAAS,KAAK;AACvD;AAAA,IACF;AAEA,UAAM,KAAK,oBAAoB,SAAS,KAAK;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,YAAY,cAAqC;AACrD,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAEA,UAAM,WAAW,KAAK,iBAAiB,YAAY,KAAK,UAAU;AAClE,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,uBAAuB,KAAK,UAAU,EAAE;AAAA,IAC1D;AAEA,QAAI,SAAS,SAAS,SAAS;AAC7B,YAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAY,KAAK,GAAG,CAAC;AAClE,YAAM,KAAK,oBAAoB,UAAU,cAAc,eAAe,aAAa;AACnF;AAAA,IACF;AAEA,UAAM,KAAK,yBAAyB,YAAY;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBACZ,UACA,SACA,OACe;AACf,UAAM,QAAQ,MAAM,KAAK,eAAe,UAAU,QAAQ;AAC1D,QAAI,CAAC,MAAO;AAEZ,UAAM,QAAQ,IAAI,WAAW,OAAO;AAAA,MAClC,WAAW;AAAA,MACX,UAAU,QAAQ;AAAA,IAAA,CACnB;AAED,SAAK,aAAa;AAAA,MAChB;AAAA,MACA,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,KAAK,iBAAiB;AAAA,MACtB,KAAK;AAAA,IAAA;AAAA,EAET;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAoB,SAAiB,OAA8B;AAC/E,UAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,UAAU;AACpD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,+BAA+B,KAAK,UAAU,EAAE;AAAA,IAClE;AAGA,UAAM,YAAY,KAAK,4BAA4B,OAAO,SAAS,KAAK;AACxE,QAAI,UAAU,KAAK,WAAW,GAAG;AAC/B;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,KAAK;AAAA,MACzB,UAAU;AAAA,MACV,UAAU;AAAA,IAAA;AAGZ,QAAI,KAAK,QAAS;AAGlB,UAAM,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO,SAAS;AAChE,QAAI,KAAK,QAAS;AAGlB,UAAM,KAAK,aAAa,QAAQ,KAAK;AAGrC,QAAI,KAAK,SAAS;AAChB,WAAK,qBAAA;AACL;AAAA,IACF;AAGA,UAAM,KAAK,mBAAmB,SAAS,KAAK;AAAA,EAC9C;AAAA,EAEA,MAAc,yBAAyB,cAAqC;AAC1E,UAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,UAAU;AACpD,QAAI,CAAC,OAAO,OAAO,OAAO;AACxB,YAAM,IAAI,MAAM,qCAAqC,KAAK,UAAU,EAAE;AAAA,IACxE;AAEA,UAAM,aAAa,MAAM,OAAO;AAChC,UAAM,EAAE,UAAU,QAAA,IAAY;AAE9B,UAAM,MAAM,kBAAkB,UAAU,cAAc,CAAC,GAAG,QAAQ;AAChE,YAAM,OAAO,SAAS,MAAM,CAAC;AAC7B,aAAO,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,KAAK,cAAc,SAAA;AAAA,IAChE,CAAC;AACD,QAAI,CAAC,KAAK;AAER,YAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAY,KAAK,GAAG,CAAC;AAClE,YAAM,KAAK,oBAAoB,cAAc,eAAe,aAAa;AACzE;AAAA,IACF;AAEA,UAAM,WAAW,IAAI;AACrB,UAAM,SAAS,KAAK,IAAI,QAAQ,QAAQ,WAAW,IAAI,WAAW;AAClE,QAAI,YAAY,OAAQ;AAGxB,QAAI,YAAY;AAChB,QAAI,UAAU;AACd,aAAS,IAAI,UAAU,IAAI,QAAQ,KAAK;AACtC,YAAM,IAAI,QAAQ,CAAC;AACnB,UAAI,CAAC,EAAG;AACR,kBAAY,KAAK,IAAI,WAAW,EAAE,UAAU;AAC5C,gBAAU,KAAK,IAAI,SAAS,EAAE,aAAa,EAAE,UAAU;AAAA,IACzD;AACA,QAAI,CAAC,OAAO,SAAS,SAAS,KAAK,WAAW,UAAW;AAEzD,UAAM,UAAU,MAAM,KAAK,8BAA8B,WAAW,OAAO;AAC3E,QAAI,KAAK,QAAS;AAElB,UAAM,SAAS,KAAK;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,YAAY,KAAK,aAAa,cAAc,OAAS;AAC3D,UAAM,EAAE,QAAQ,MAAA,IAAU,MAAM;AAAA,MAC9B;AAAA,MACA;AAAA,QACE,OAAO,WAAW;AAAA,QAClB,OAAO,WAAW;AAAA,QAClB,QAAQ,WAAW;AAAA,QACnB,aAAa,WAAW;AAAA,MAAA;AAAA,MAE1B;AAAA,MACA;AAAA,QACE;AAAA,QACA,cAAc;AAAA,QACd,aAAa,MAAM,KAAK;AAAA,MAAA;AAAA,IAC1B;AAGF,QAAI,KAAK,SAAS;AAChB,cAAQ,MAAA;AACR,aAAO,MAAA;AACP;AAAA,IACF;AAGA,eAAW,SAAS,CAAC,QAAQ,KAAK,GAAG;AACnC,UAAI,CAAC,MAAO;AACZ,UAAI;AACF,aAAK,WAAW,KAAK;AAAA,MACvB,QAAQ;AACN,cAAM,MAAA;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,8BACN,MACA,gBACA,SACA,UACA,QACqB;AACrB,UAAM,SAA8B,CAAA;AACpC,UAAM,WAAW,IAAI,WAAW,IAAI;AAEpC,aAAS,IAAI,UAAU,IAAI,QAAQ,KAAK;AACtC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,CAAC,OAAQ;AAEb,YAAM,iBAAiB,OAAO,aAAa;AAC3C,UAAI,iBAAiB,KAAK,iBAAiB,OAAO,aAAa,KAAK,YAAY;AAC9E;AAAA,MACF;AAEA,YAAM,aAAa,SAAS,MAAM,gBAAgB,iBAAiB,OAAO,UAAU;AACpF,aAAO;AAAA,QACL,IAAI,kBAAkB;AAAA,UACpB,MAAM,OAAO,aAAa,QAAQ;AAAA,UAClC,WAAW,OAAO;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,MAAM;AAAA,QAAA,CACP;AAAA,MAAA;AAAA,IAEL;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,uBAA6B;AACnC,eAAW,SAAS,KAAK,eAAe;AACtC,YAAM,MAAA;AAAA,IACR;AACA,SAAK,gBAAgB,CAAA;AAAA,EACvB;AAAA,EAEQ,4BACN,OACA,SACA,OACiB;AACjB,QAAI,CAAC,MAAM,OAAO,OAAO;AACvB,aAAO,EAAE,MAAM,CAAA,GAAI,WAAW,GAAG,SAAS,EAAA;AAAA,IAC5C;AAEA,UAAM,EAAE,UAAU,QAAA,IAAY,MAAM,OAAO;AAG3C,UAAM,kBAAkB,KAAK,cAAc,mBAAmB,KAAK,YAAY,OAAO;AACtF,UAAM,gBAAgB,iBAAiB,aAAa;AAGpD,UAAM,kBAAkB,wBAAwB,UAAU,eAAe,OAAO,CAAC,KAAK,QAAQ;AAC5F,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,gBAAgB,WAAW,GAAG;AAChC,aAAO,EAAE,MAAM,CAAA,GAAI,WAAW,GAAG,SAAS,EAAA;AAAA,IAC5C;AAGA,QAAI,YAAY;AAChB,QAAI,UAAU;AAEd,eAAW,OAAO,iBAAiB;AACjC,YAAM,cAAc,QAAQ,IAAI,mBAAmB;AACnD,YAAM,iBAAiB,IAAI,sBAAsB,IAAI,cAAc;AACnE,YAAM,YAAY,QAAQ,cAAc;AAExC,UAAI,eAAe,WAAW;AAC5B,oBAAY,KAAK,IAAI,WAAW,YAAY,UAAU;AACtD,kBAAU,KAAK,IAAI,SAAS,UAAU,aAAa,UAAU,UAAU;AAAA,MACzE;AAAA,IACF;AAEA,WAAO,EAAE,MAAM,iBAAiB,WAAW,QAAA;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,aACZ,MACA,OACA,WAC8B;AAC9B,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAEA,UAAM,EAAE,YAAY;AACpB,UAAM,SAA8B,CAAA;AACpC,UAAM,WAAW,IAAI,WAAW,IAAI;AACpC,UAAM,iBAAiB,UAAU;AAGjC,eAAW,OAAO,UAAU,MAAM;AAChC,YAAM,WAAW,IAAI;AACrB,YAAM,SAAS,WAAW,IAAI;AAG9B,eAAS,IAAI,UAAU,IAAI,QAAQ,KAAK;AACtC,cAAM,SAAS,QAAQ,CAAC;AACxB,YAAI,CAAC,OAAQ;AAGb,cAAM,iBAAiB,OAAO,aAAa;AAG3C,YAAI,iBAAiB,KAAK,iBAAiB,OAAO,aAAa,KAAK,YAAY;AAC9E,kBAAQ,KAAK,iDAAiD;AAAA,YAC5D,cAAc,OAAO;AAAA,YACrB,cAAc,OAAO;AAAA,YACrB,YAAY;AAAA,YACZ;AAAA,YACA,cAAc,KAAK;AAAA,UAAA,CACpB;AACD;AAAA,QACF;AAGA,cAAM,aAAa,SAAS,MAAM,gBAAgB,iBAAiB,OAAO,UAAU;AAGpF,cAAM,QAAQ,IAAI,kBAAkB;AAAA,UAClC,MAAM,OAAO,aAAa,QAAQ;AAAA,UAClC,WAAW,OAAO;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,MAAM;AAAA,QAAA,CACP;AAED,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,aAAa,QAA6B,OAAgC;AACtF,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAEA,UAAM,YAAY,KAAK,aAAa,cAAc,OAAS;AAC3D,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA;AAAA,QACE,OAAO,WAAW;AAAA,QAClB,OAAO,WAAW;AAAA,QAClB,QAAQ,WAAW;AAAA,QACnB,aAAa,WAAW;AAAA,MAAA;AAAA,MAE1B,YAAY,EAAE,cAAc;AAAA,IAAA;AAI9B,SAAK,gBAAgB,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAW,OAAmB,mBAA2B,GAAS;AACxE,UAAM,gBAAgB,MAAM,YAAY,KAAK,MAAM,MAAY,KAAK,GAAG;AACvE,UAAM,kBACJ,KAAK,gBAAgB,MAAM,YAAY,KAAK,gBAAgB;AAE9D,SAAK,aAAa;AAAA,MAChB;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA,KAAK,iBAAiB;AAAA,MACtB;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAc,mBAAmB,SAAiB,OAA8B;AAC9E,UAAM,gBAA8B,CAAA;AACpC,UAAM,kBAAgC,CAAA;AAGtC,eAAW,SAAS,KAAK,eAAe;AACtC,UAAI,MAAM,aAAa,WAAW,MAAM,YAAY,OAAO;AACzD,sBAAc,KAAK,KAAK;AAAA,MAC1B,OAAO;AACL,wBAAgB,KAAK,KAAK;AAAA,MAC5B;AAAA,IACF;AAGA,eAAW,SAAS,eAAe;AACjC,UAAI;AACF,aAAK,WAAW,KAAK;AAAA,MACvB,QAAQ;AACN,cAAM,MAAA;AAAA,MACR;AAAA,IACF;AAGA,eAAW,SAAS,iBAAiB;AACnC,YAAM,MAAA;AAAA,IACR;AAEA,SAAK,gBAAgB,CAAA;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,oBAAoB,SAAiB,OAAoD;AAC7F,UAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,UAAU;AACpD,QAAI,CAAC,OAAO,OAAO,OAAO;AACxB,YAAM,IAAI,MAAM,mDAAmD,KAAK,UAAU,EAAE;AAAA,IACtF;AAEA,UAAM,aAAa,MAAM,OAAO;AAChC,UAAM,YAAY,KAAK,4BAA4B,OAAO,SAAS,KAAK;AAExE,WAAO,IAAI,eAA2B;AAAA,MACpC,OAAO,OAAO,eAAe;AAC3B,YAAI;AACF,cAAI,UAAU,KAAK,WAAW,GAAG;AAC/B,oBAAQ,KAAK,gDAAgD;AAC7D,uBAAW,MAAA;AACX;AAAA,UACF;AAGA,gBAAM,YAAY;AAClB,mBAAS,IAAI,GAAG,IAAI,UAAU,KAAK,QAAQ,KAAK,WAAW;AACzD,gBAAI,KAAK,SAAS;AAChB,yBAAW,MAAA;AACX;AAAA,YACF;AAEA,kBAAM,YAAY,UAAU,KAAK;AAAA,cAC/B;AAAA,cACA,KAAK,IAAI,IAAI,WAAW,UAAU,KAAK,MAAM;AAAA,YAAA;AAI/C,gBAAI,iBAAiB;AACrB,gBAAI,eAAe;AACnB,uBAAW,OAAO,WAAW;AAC3B,oBAAM,cAAc,WAAW,QAAQ,IAAI,mBAAmB;AAC9D,oBAAM,eAAe,IAAI,sBAAsB,IAAI,cAAc;AACjE,oBAAM,YAAY,WAAW,QAAQ,YAAY;AACjD,kBAAI,eAAe,WAAW;AAC5B,iCAAiB,KAAK,IAAI,gBAAgB,YAAY,UAAU;AAChE,+BAAe,KAAK,IAAI,cAAc,UAAU,aAAa,UAAU,UAAU;AAAA,cACnF;AAAA,YACF;AAGA,kBAAM,UAAU,MAAM,KAAK,8BAA8B,gBAAgB,YAAY;AAErF,gBAAI,KAAK,SAAS;AAChB,yBAAW,MAAA;AACX;AAAA,YACF;AAGA,kBAAM,cAAc,MAAM,KAAK,aAAa,SAAS,OAAO;AAAA,cAC1D,MAAM;AAAA,cACN,WAAW;AAAA,cACX,SAAS;AAAA,YAAA,CACV;AAGD,kBAAM,KAAK,aAAa,aAAa,KAAK;AAE1C,gBAAI,KAAK,SAAS;AAChB,mBAAK,qBAAA;AACL,yBAAW,MAAA;AACX;AAAA,YACF;AAGA,uBAAW,SAAS,KAAK,eAAe;AACtC,yBAAW,QAAQ,KAAK;AAAA,YAC1B;AAGA,iBAAK,gBAAgB,CAAA;AAAA,UACvB;AAEA,qBAAW,MAAA;AAAA,QACb,SAAS,OAAO;AACd,kBAAQ,MAAM,qDAAqD,KAAK;AACxE,eAAK,qBAAA;AACL,qBAAW,MAAM,KAAK;AAAA,QACxB;AAAA,MACF;AAAA,MACA,QAAQ,MAAM;AACZ,aAAK,UAAU;AACf,aAAK,qBAAA;AAAA,MACP;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEA,MAAc,8BAA8B,OAAe,KAAmC;AAC5F,QAAI;AACF,aAAO,MAAM,KAAK,aAAa,kBAAkB,KAAK,YAAY,OAAO,GAAG;AAAA,IAC9E,SAAS,OAAO;AACd,UAAI,EAAE,iBAAiB,wBAAyB,OAAM;AAMtD,YAAM,KAAK,aAAa,cAAc,eAAe,KAAK,UAAU;AACpE,WAAK,aAAa,cAAc,OAAO,KAAK,UAAU;AAEtD,YAAM,KAAK,eAAe,KAAK,KAAK,YAAY,EAAE,WAAW,OAAO,QAAQ,KAAK,OAAA,CAAQ;AACzF,aAAO,MAAM,KAAK,aAAa,kBAAkB,KAAK,YAAY,OAAO,GAAG;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,WAAY;AAErB,SAAK,UAAU;AAGf,SAAK,qBAAA;AAEL,SAAK,aAAa;AAAA,EACpB;AACF;"}
@@ -40,11 +40,6 @@ export declare class Orchestrator implements IOrchestrator {
40
40
  off<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
41
41
  once<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
42
42
  cancelActiveDecoding(): void;
43
- /**
44
- * Decode and render nearest keyframe for fast seek preview
45
- * Returns the keyframe timestamp if successfully decoded, null otherwise
46
- */
47
- tryRenderKeyframe(timeUs: TimeUs): Promise<TimeUs | null>;
48
43
  setCompositionModel(model: CompositionModel): Promise<void>;
49
44
  applyPatch(patch: CompositionPatch): Promise<void>;
50
45
  private handleResourceStateChange;
@@ -1 +1 @@
1
- {"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAErF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAY,MAAM,EAAE,OAAO,EAAQ,MAAM,UAAU,CAAC;AAE/F,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIpD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACjD,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,KAAK,EAAE;QACL,OAAO,EAAE,mBAAmB,CAAC;QAC7B,OAAO,EAAE,mBAAmB,CAAC;QAC7B,MAAM,EAAE,kBAAkB,CAAC;KAC5B,CAAC;IACF,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IAEjC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,wBAAwB,CAAuB;IACvD,OAAO,CAAC,qBAAqB,CAAqC;IAClE,OAAO,CAAC,UAAU,CAAK;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;gBAE5D,MAAM,EAAE,kBAAkB;IAgFtC,OAAO,CAAC,8BAA8B;IA0BhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IASjC,EAAE,CAAC,CAAC,SAAS,MAAM,eAAe,EAChC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAC7C,IAAI;IAIP,GAAG,CAAC,CAAC,SAAS,MAAM,eAAe,EACjC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAC7C,IAAI;IAIP,IAAI,CAAC,CAAC,SAAS,MAAM,eAAe,EAClC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAC7C,IAAI;IAIP,oBAAoB,IAAI,IAAI;IAOtB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B3D,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCxD,OAAO,CAAC,yBAAyB;IAgB3B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IA6DrF;;;;;;;OAOG;YACW,kBAAkB;IAwHhC;;;OAGG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GACvD,OAAO,CAAC,OAAO,CAAC;IAqBb,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B,OAAO,CAAC,kBAAkB;IAkD1B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAsBzB,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAInF;;;;;;;;OAQG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,MAAM,EACzB,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAwDhB;;;OAGG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC;QAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,IAAI,CAAC;IAgFtD;;OAEG;YACW,gBAAgB;CAmG/B"}
@@ -134,48 +134,6 @@ class Orchestrator {
134
134
  this.activeOnDemandSession = null;
135
135
  }
136
136
  }
137
- /**
138
- * Decode and render nearest keyframe for fast seek preview
139
- * Returns the keyframe timestamp if successfully decoded, null otherwise
140
- */
141
- async tryRenderKeyframe(timeUs) {
142
- if (!this.compositionModel) return null;
143
- const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];
144
- if (!clip || !hasResourceId(clip)) return null;
145
- const resourceTimeUs = timeUs - clip.startUs + (clip.trimStartUs ?? 0);
146
- const resourceId = clip.resourceId;
147
- const keyframeSample = this.cacheManager.mp4IndexCache.getNearestKeyframe(
148
- resourceId,
149
- resourceTimeUs
150
- );
151
- if (!keyframeSample) return null;
152
- const cachedKeyframe = this.cacheManager.getFrame(keyframeSample.timestamp, clip.id);
153
- if (cachedKeyframe) {
154
- return keyframeSample.timestamp;
155
- }
156
- const resource = this.compositionModel.getResource(resourceId);
157
- if (resource?.state !== "ready") return null;
158
- const session = await OnDemandVideoSession.create({
159
- clipId: clip.id,
160
- resourceId,
161
- targetTimeUs: resourceTimeUs,
162
- globalTimeUs: timeUs,
163
- resourceLoader: this.resourceLoader,
164
- mp4IndexCache: this.cacheManager.mp4IndexCache,
165
- cacheManager: this.cacheManager,
166
- compositionModel: this.compositionModel,
167
- fps: this.compositionModel.fps ?? 30
168
- });
169
- try {
170
- const keyframeTimeUs = await session.decodeKeyframe(resourceTimeUs);
171
- return keyframeTimeUs;
172
- } catch (error) {
173
- console.warn("[Orchestrator] Fast keyframe decode failed:", error);
174
- return null;
175
- } finally {
176
- await session.dispose();
177
- }
178
- }
179
137
  async setCompositionModel(model) {
180
138
  this.cacheManager.clear();
181
139
  this.cancelActiveDecoding();
@@ -227,6 +185,7 @@ class Orchestrator {
227
185
  async getFrame(timeUs, options) {
228
186
  const signal = options?.signal;
229
187
  const preheat = options?.preheat ?? false;
188
+ const scrub = options?.scrub ?? false;
230
189
  const mode = options?.mode ?? "blocking";
231
190
  if (!this.compositionModel) {
232
191
  throw new Error("No composition model set");
@@ -241,7 +200,16 @@ class Orchestrator {
241
200
  if (!preheat) {
242
201
  const cachedFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);
243
202
  if (cachedFrame) {
244
- return cachedFrame;
203
+ if (scrub) {
204
+ const fps = this.compositionModel?.fps ?? 30;
205
+ const maxDistanceUs = Math.max(1, Math.round(1e6 / fps));
206
+ const cachedTs = cachedFrame.timestampUs ?? 0;
207
+ if (Math.abs(cachedTs - resourceTimeUs) <= maxDistanceUs) {
208
+ return cachedFrame;
209
+ }
210
+ } else {
211
+ return cachedFrame;
212
+ }
245
213
  }
246
214
  }
247
215
  if (signal?.aborted) {
@@ -323,6 +291,10 @@ class Orchestrator {
323
291
  });
324
292
  this.activeOnDemandSession = session;
325
293
  try {
294
+ if (options?.scrub) {
295
+ await session.decodeScrub(resourceTimeUs);
296
+ return this.cacheManager.getFrame(resourceTimeUs, clip.id);
297
+ }
326
298
  const DECODE_WINDOW_SIZE = 3e6;
327
299
  const windowStart = resourceTimeUs;
328
300
  const windowEnd = Math.min(