@meframe/core 0.2.5 → 0.2.7

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 (26) hide show
  1. package/dist/medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
  2. package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +2 -2
  3. package/dist/medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
  4. package/dist/model/types.d.ts +7 -0
  5. package/dist/model/types.d.ts.map +1 -1
  6. package/dist/model/types.js.map +1 -1
  7. package/dist/orchestrator/AudioWindowPreparer.d.ts.map +1 -1
  8. package/dist/orchestrator/AudioWindowPreparer.js +23 -3
  9. package/dist/orchestrator/AudioWindowPreparer.js.map +1 -1
  10. package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
  11. package/dist/stages/compose/OfflineAudioMixer.js +47 -28
  12. package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
  13. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
  14. package/dist/stages/demux/MP4IndexParser.js +4 -2
  15. package/dist/stages/demux/MP4IndexParser.js.map +1 -1
  16. package/dist/stages/mux/MP4Muxer.js +1 -1
  17. package/dist/utils/loop-utils.d.ts +16 -0
  18. package/dist/utils/loop-utils.d.ts.map +1 -0
  19. package/dist/utils/loop-utils.js +44 -0
  20. package/dist/utils/loop-utils.js.map +1 -0
  21. package/dist/utils/mp4box.js +1 -1
  22. package/dist/workers/MP4Demuxer.DfWiwyjB.js.map +1 -1
  23. package/package.json +1 -1
  24. package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +0 -1
  25. package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +0 -1
  26. /package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +0 -0
@@ -33,6 +33,13 @@ interface BaseClip {
33
33
  durationUs: TimeUs;
34
34
  trackId?: string;
35
35
  trimStartUs?: TimeUs;
36
+ /**
37
+ * Loop the underlying media content to fill this clip's duration.
38
+ *
39
+ * - AudioClip: loop audio samples
40
+ * - VideoClip: (future) loop video frames (and embedded audio) together
41
+ */
42
+ loop?: boolean;
36
43
  effects?: Effect[];
37
44
  attachments?: Attachment[];
38
45
  transitionIn?: Transition;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAG5B,eAAO,MAAM,uBAAuB,UAAY,CAAC;AACjD,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAGlD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,KAAK,CAAC;IACf,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAEpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAE5B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAGD,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;IACvD,KAAK,EAAE,IAAI,EAAE,CAAC;IAEd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;CAC9B;AAKD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAE3B,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,aAAa,CAAC,EAAE,UAAU,CAAC;IAE3B,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;QACzC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IAGF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IAGnB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAGhC,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IAGnB,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,SAAS,CAAC,EAAE;QACV,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,cAAc,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;QAChG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;CACzC;AAED,MAAM,WAAW,MAAO,SAAQ,QAAQ;IACtC,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IAGnB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAGhC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,MAAM,CAAC;AAG9E,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;CACnD;AAGD,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,QAAQ,GAAG,KAAK,GAAG,WAAW,GAAG,MAAM,CAAC;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAGD,MAAM,WAAW,eAAgB,SAAQ,MAAM;IAC7C,UAAU,EAAE,WAAW,CAAC;IACxB,MAAM,EAAE;QACN,QAAQ,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnC,SAAS,EAAE,iBAAiB,EAAE,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,SAAS,CAAC,EAAE;QACV,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,cAAc,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;QAChG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE;QACT,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,MAAM,cAAc,GACtB,cAAc,GACd,aAAa,GACb,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,eAAe,GACf,qBAAqB,CAAC;AAG1B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,GAAG,aAAa,GAAG,aAAa,CAAC;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;CACxB;AAGD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,CAAC;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9B;AAGD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAClC;AAGD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,IAAI,GAAG,KAAK,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAClC;AAGD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;CACtC;AAGD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;IACpD,UAAU,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAGD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAGD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,GAAG,CAAC;CACZ;AAGD,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,SAAS,CAEzD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,SAAS,CAEzD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,WAAW,CAE7D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,MAAM,CAEnD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,SAAS,GAAG,SAAS,CAEvE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,SAAS,GAAG,SAAS,CAExE"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../src/model/types.ts"],"sourcesContent":["// All time values in microseconds (µs)\nexport type TimeUs = number; // 1 second = 1_000_000 µs\n\n// Helper constants\nexport const MICROSECONDS_PER_SECOND = 1_000_000;\nexport const MICROSECONDS_PER_MILLISECOND = 1_000;\n\n// ────── Root Object ──────\nexport interface CompositionModelData {\n version: '1.0';\n fps: 24 | 25 | 30 | 60;\n durationUs: TimeUs;\n tracks: Track[];\n resources: Record<string, Resource>;\n\n mainTrackId?: string;\n renderConfig?: RenderConfig;\n\n ext?: Record<string, unknown>;\n}\n\nexport interface RenderConfig {\n width: number;\n height: number;\n backgroundColor?: string;\n}\n\n// ────── Track ──────\nexport interface Track {\n id: string;\n kind: 'video' | 'audio' | 'caption' | 'overlay' | 'fx';\n clips: Clip[];\n\n effects?: Effect[];\n duckingRules?: DuckingRule[];\n}\n\n// ────── Clip ──────\n\n// Clip-level render configuration for video/image resources\nexport interface ClipRenderConfig {\n width?: number | string; // Pixels or percentage (e.g., \"10%\")\n height?: number | string;\n}\n\ninterface BaseClip {\n id: string;\n startUs: TimeUs;\n durationUs: TimeUs;\n trackId?: string;\n\n trimStartUs?: TimeUs;\n\n effects?: Effect[];\n attachments?: Attachment[];\n\n transitionIn?: Transition;\n transitionOut?: Transition;\n\n metadata?: {\n purpose?: 'caption' | 'overlay' | 'mask';\n [key: string]: unknown;\n };\n\n // Internal: temporary field for tracking old resourceId during patch operations\n oldResourceId?: string;\n}\n\nexport interface VideoClip extends BaseClip {\n trackKind: 'video';\n resourceId: string;\n\n // Render configuration (size, positioning strategy)\n renderConfig?: ClipRenderConfig;\n\n // Audio configuration for video's original sound\n audioConfig?: {\n volume?: number; // 0.0-1.0, default 1.0\n muted?: boolean; // default false\n };\n}\n\nexport interface AudioClip extends BaseClip {\n trackKind: 'audio';\n resourceId: string;\n\n // Audio configuration\n audioConfig?: {\n volume?: number; // 0.0-1.0, default 1.0\n muted?: boolean; // default false\n };\n}\n\nexport interface CaptionClip extends BaseClip {\n trackKind: 'caption';\n text: string;\n localeCode?: string;\n fontTemplate?: string;\n fontFamily?: string;\n wordTimings?: Array<{\n text: string;\n startUs: TimeUs;\n endUs: TimeUs;\n }>;\n animation?: {\n type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';\n [key: string]: unknown;\n };\n letterCase?: 'upper' | 'lower' | 'none';\n}\n\nexport interface FxClip extends BaseClip {\n trackKind: 'fx';\n}\n\nexport interface OverlayClip extends BaseClip {\n trackKind: 'overlay';\n resourceId: string;\n\n // Render configuration (size, positioning strategy)\n renderConfig?: ClipRenderConfig;\n\n // Optional overlay-specific config\n opacity?: number; // 0.0-1.0, default 1.0\n}\n\nexport type Clip = VideoClip | AudioClip | CaptionClip | OverlayClip | FxClip;\n\n// ────── Resource ──────\nexport interface Resource {\n id: string;\n type: 'video' | 'image' | 'audio' | 'json' | string;\n uri: string;\n metadata?: Record<string, unknown>;\n clipIds?: string[];\n // Runtime state maintained by engine\n state?: 'pending' | 'loading' | 'ready' | 'error';\n}\n\n// ────── Common Structures ──────\nexport interface Effect {\n id: string;\n effectType: 'filter' | 'lut' | 'animation' | string;\n params?: Record<string, unknown>;\n}\n\n// Animation effect (effectType: 'animation')\nexport interface AnimationEffect extends Effect {\n effectType: 'animation';\n params: {\n position: { x: number; y: number };\n keyframes: AnimationKeyframe[];\n };\n}\n\nexport interface AnimationKeyframe {\n time: number; // Relative to clip.startUs (microseconds)\n transform?: Transform2D;\n opacity?: number;\n easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';\n}\n\nexport interface Transform2D {\n x?: number; // Relative offset\n y?: number;\n scaleX?: number;\n scaleY?: number;\n rotation?: number; // Degrees\n anchorX?: number; // Rotation center x (0-1)\n anchorY?: number; // Rotation center y (0-1)\n}\n\nexport interface Transition {\n id: string;\n transitionType: 'fade' | 'wipe' | 'slide' | string;\n durationUs: TimeUs;\n curve?: 'linear' | 'ease-in' | 'ease-out' | string;\n params?: Record<string, unknown>;\n}\n\nexport interface Attachment {\n id: string;\n kind: 'caption' | 'overlay' | 'mask' | string;\n startUs: TimeUs;\n durationUs: TimeUs;\n data: Record<string, unknown>;\n}\n\nexport interface CaptionAttachmentData {\n text: string;\n localeCode?: string;\n fontTemplate?: string;\n fontFamily?: string;\n wordTimings?: Array<{\n text: string;\n startUs: TimeUs;\n endUs: TimeUs;\n }>;\n animation?: {\n type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';\n [key: string]: unknown;\n };\n letterCase?: 'upper' | 'lower' | 'none';\n overlayClipStartUs?: TimeUs;\n mainClipStartUs?: TimeUs;\n}\n\nexport interface DuckingRule {\n targetTrackKind: 'voice' | 'audio' | string;\n ratio: number;\n attackMs: number;\n releaseMs: number;\n}\n\n// ────── Patch System ──────\nexport interface CompositionPatch {\n operations: PatchOperation[];\n metadata?: {\n timestamp: number;\n source?: string;\n version?: string;\n };\n}\n\nexport type PatchOperation =\n | TrackOperation\n | ClipOperation\n | ResourceOperation\n | AttachmentOperation\n | TransitionOperation\n | EffectOperation\n | RenderConfigOperation;\n\n// Track operations\nexport interface TrackOperation {\n type: 'addTrack' | 'updateTrack' | 'removeTrack';\n trackId?: string;\n track?: Partial<Track>;\n}\n\n// Clip operations\nexport interface ClipOperation {\n type: 'addClip' | 'updateClip' | 'removeClip' | 'moveClip';\n trackId: string;\n clipId?: string;\n clip?: Partial<Clip>;\n targetTrackId?: string;\n targetStartUs?: TimeUs;\n}\n\n// Resource operations\nexport interface ResourceOperation {\n type: 'addResource' | 'updateResource' | 'removeResource';\n resourceId: string;\n resource?: Partial<Resource>;\n}\n\n// Attachment operations\nexport interface AttachmentOperation {\n type: 'addAttachment' | 'updateAttachment' | 'removeAttachment';\n trackId: string;\n clipId: string;\n attachmentId?: string;\n attachment?: Partial<Attachment>;\n}\n\n// Transition operations\nexport interface TransitionOperation {\n type: 'addTransition' | 'updateTransition' | 'removeTransition';\n trackId: string;\n clipId: string;\n position: 'in' | 'out';\n transition?: Partial<Transition>;\n}\n\n// Render config operations\nexport interface RenderConfigOperation {\n type: 'updateRenderConfig';\n renderConfig?: Partial<RenderConfig>;\n}\n\n// Effect operations\nexport interface EffectOperation {\n type: 'addEffect' | 'updateEffect' | 'removeEffect';\n targetType: 'track' | 'clip';\n targetId: string;\n effectId?: string;\n effect?: Partial<Effect>;\n}\n\n// ────── Dirty Range ──────\nexport interface DirtyRange {\n trackId: string;\n startUs: TimeUs;\n endUs: TimeUs;\n reason: string;\n}\n\n// ────── Validation ──────\nexport interface ValidationError {\n path: string;\n message: string;\n value: any;\n}\n\n// ────── Type Guards ──────\nexport function isVideoClip(clip: Clip): clip is VideoClip {\n return clip.trackKind === 'video';\n}\n\nexport function isAudioClip(clip: Clip): clip is AudioClip {\n return clip.trackKind === 'audio';\n}\n\nexport function isCaptionClip(clip: Clip): clip is CaptionClip {\n return clip.trackKind === 'caption';\n}\n\nexport function isFxClip(clip: Clip): clip is FxClip {\n return clip.trackKind === 'fx';\n}\n\nexport function hasResourceId(clip: Clip): clip is VideoClip | AudioClip {\n return isVideoClip(clip) || isAudioClip(clip);\n}\n\nexport function hasAudioConfig(clip: Clip): clip is VideoClip | AudioClip {\n return isVideoClip(clip) || isAudioClip(clip);\n}\n"],"names":[],"mappings":"AAkTO,SAAS,YAAY,MAA+B;AACzD,SAAO,KAAK,cAAc;AAC5B;AAEO,SAAS,YAAY,MAA+B;AACzD,SAAO,KAAK,cAAc;AAC5B;AAUO,SAAS,cAAc,MAA2C;AACvE,SAAO,YAAY,IAAI,KAAK,YAAY,IAAI;AAC9C;AAEO,SAAS,eAAe,MAA2C;AACxE,SAAO,YAAY,IAAI,KAAK,YAAY,IAAI;AAC9C;"}
1
+ {"version":3,"file":"types.js","sources":["../../src/model/types.ts"],"sourcesContent":["// All time values in microseconds (µs)\nexport type TimeUs = number; // 1 second = 1_000_000 µs\n\n// Helper constants\nexport const MICROSECONDS_PER_SECOND = 1_000_000;\nexport const MICROSECONDS_PER_MILLISECOND = 1_000;\n\n// ────── Root Object ──────\nexport interface CompositionModelData {\n version: '1.0';\n fps: 24 | 25 | 30 | 60;\n durationUs: TimeUs;\n tracks: Track[];\n resources: Record<string, Resource>;\n\n mainTrackId?: string;\n renderConfig?: RenderConfig;\n\n ext?: Record<string, unknown>;\n}\n\nexport interface RenderConfig {\n width: number;\n height: number;\n backgroundColor?: string;\n}\n\n// ────── Track ──────\nexport interface Track {\n id: string;\n kind: 'video' | 'audio' | 'caption' | 'overlay' | 'fx';\n clips: Clip[];\n\n effects?: Effect[];\n duckingRules?: DuckingRule[];\n}\n\n// ────── Clip ──────\n\n// Clip-level render configuration for video/image resources\nexport interface ClipRenderConfig {\n width?: number | string; // Pixels or percentage (e.g., \"10%\")\n height?: number | string;\n}\n\ninterface BaseClip {\n id: string;\n startUs: TimeUs;\n durationUs: TimeUs;\n trackId?: string;\n\n trimStartUs?: TimeUs;\n /**\n * Loop the underlying media content to fill this clip's duration.\n *\n * - AudioClip: loop audio samples\n * - VideoClip: (future) loop video frames (and embedded audio) together\n */\n loop?: boolean;\n\n effects?: Effect[];\n attachments?: Attachment[];\n\n transitionIn?: Transition;\n transitionOut?: Transition;\n\n metadata?: {\n purpose?: 'caption' | 'overlay' | 'mask';\n [key: string]: unknown;\n };\n\n // Internal: temporary field for tracking old resourceId during patch operations\n oldResourceId?: string;\n}\n\nexport interface VideoClip extends BaseClip {\n trackKind: 'video';\n resourceId: string;\n\n // Render configuration (size, positioning strategy)\n renderConfig?: ClipRenderConfig;\n\n // Audio configuration for video's original sound\n audioConfig?: {\n volume?: number; // 0.0-1.0, default 1.0\n muted?: boolean; // default false\n };\n}\n\nexport interface AudioClip extends BaseClip {\n trackKind: 'audio';\n resourceId: string;\n\n // Audio configuration\n audioConfig?: {\n volume?: number; // 0.0-1.0, default 1.0\n muted?: boolean; // default false\n };\n}\n\nexport interface CaptionClip extends BaseClip {\n trackKind: 'caption';\n text: string;\n localeCode?: string;\n fontTemplate?: string;\n fontFamily?: string;\n wordTimings?: Array<{\n text: string;\n startUs: TimeUs;\n endUs: TimeUs;\n }>;\n animation?: {\n type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';\n [key: string]: unknown;\n };\n letterCase?: 'upper' | 'lower' | 'none';\n}\n\nexport interface FxClip extends BaseClip {\n trackKind: 'fx';\n}\n\nexport interface OverlayClip extends BaseClip {\n trackKind: 'overlay';\n resourceId: string;\n\n // Render configuration (size, positioning strategy)\n renderConfig?: ClipRenderConfig;\n\n // Optional overlay-specific config\n opacity?: number; // 0.0-1.0, default 1.0\n}\n\nexport type Clip = VideoClip | AudioClip | CaptionClip | OverlayClip | FxClip;\n\n// ────── Resource ──────\nexport interface Resource {\n id: string;\n type: 'video' | 'image' | 'audio' | 'json' | string;\n uri: string;\n metadata?: Record<string, unknown>;\n clipIds?: string[];\n // Runtime state maintained by engine\n state?: 'pending' | 'loading' | 'ready' | 'error';\n}\n\n// ────── Common Structures ──────\nexport interface Effect {\n id: string;\n effectType: 'filter' | 'lut' | 'animation' | string;\n params?: Record<string, unknown>;\n}\n\n// Animation effect (effectType: 'animation')\nexport interface AnimationEffect extends Effect {\n effectType: 'animation';\n params: {\n position: { x: number; y: number };\n keyframes: AnimationKeyframe[];\n };\n}\n\nexport interface AnimationKeyframe {\n time: number; // Relative to clip.startUs (microseconds)\n transform?: Transform2D;\n opacity?: number;\n easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';\n}\n\nexport interface Transform2D {\n x?: number; // Relative offset\n y?: number;\n scaleX?: number;\n scaleY?: number;\n rotation?: number; // Degrees\n anchorX?: number; // Rotation center x (0-1)\n anchorY?: number; // Rotation center y (0-1)\n}\n\nexport interface Transition {\n id: string;\n transitionType: 'fade' | 'wipe' | 'slide' | string;\n durationUs: TimeUs;\n curve?: 'linear' | 'ease-in' | 'ease-out' | string;\n params?: Record<string, unknown>;\n}\n\nexport interface Attachment {\n id: string;\n kind: 'caption' | 'overlay' | 'mask' | string;\n startUs: TimeUs;\n durationUs: TimeUs;\n data: Record<string, unknown>;\n}\n\nexport interface CaptionAttachmentData {\n text: string;\n localeCode?: string;\n fontTemplate?: string;\n fontFamily?: string;\n wordTimings?: Array<{\n text: string;\n startUs: TimeUs;\n endUs: TimeUs;\n }>;\n animation?: {\n type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';\n [key: string]: unknown;\n };\n letterCase?: 'upper' | 'lower' | 'none';\n overlayClipStartUs?: TimeUs;\n mainClipStartUs?: TimeUs;\n}\n\nexport interface DuckingRule {\n targetTrackKind: 'voice' | 'audio' | string;\n ratio: number;\n attackMs: number;\n releaseMs: number;\n}\n\n// ────── Patch System ──────\nexport interface CompositionPatch {\n operations: PatchOperation[];\n metadata?: {\n timestamp: number;\n source?: string;\n version?: string;\n };\n}\n\nexport type PatchOperation =\n | TrackOperation\n | ClipOperation\n | ResourceOperation\n | AttachmentOperation\n | TransitionOperation\n | EffectOperation\n | RenderConfigOperation;\n\n// Track operations\nexport interface TrackOperation {\n type: 'addTrack' | 'updateTrack' | 'removeTrack';\n trackId?: string;\n track?: Partial<Track>;\n}\n\n// Clip operations\nexport interface ClipOperation {\n type: 'addClip' | 'updateClip' | 'removeClip' | 'moveClip';\n trackId: string;\n clipId?: string;\n clip?: Partial<Clip>;\n targetTrackId?: string;\n targetStartUs?: TimeUs;\n}\n\n// Resource operations\nexport interface ResourceOperation {\n type: 'addResource' | 'updateResource' | 'removeResource';\n resourceId: string;\n resource?: Partial<Resource>;\n}\n\n// Attachment operations\nexport interface AttachmentOperation {\n type: 'addAttachment' | 'updateAttachment' | 'removeAttachment';\n trackId: string;\n clipId: string;\n attachmentId?: string;\n attachment?: Partial<Attachment>;\n}\n\n// Transition operations\nexport interface TransitionOperation {\n type: 'addTransition' | 'updateTransition' | 'removeTransition';\n trackId: string;\n clipId: string;\n position: 'in' | 'out';\n transition?: Partial<Transition>;\n}\n\n// Render config operations\nexport interface RenderConfigOperation {\n type: 'updateRenderConfig';\n renderConfig?: Partial<RenderConfig>;\n}\n\n// Effect operations\nexport interface EffectOperation {\n type: 'addEffect' | 'updateEffect' | 'removeEffect';\n targetType: 'track' | 'clip';\n targetId: string;\n effectId?: string;\n effect?: Partial<Effect>;\n}\n\n// ────── Dirty Range ──────\nexport interface DirtyRange {\n trackId: string;\n startUs: TimeUs;\n endUs: TimeUs;\n reason: string;\n}\n\n// ────── Validation ──────\nexport interface ValidationError {\n path: string;\n message: string;\n value: any;\n}\n\n// ────── Type Guards ──────\nexport function isVideoClip(clip: Clip): clip is VideoClip {\n return clip.trackKind === 'video';\n}\n\nexport function isAudioClip(clip: Clip): clip is AudioClip {\n return clip.trackKind === 'audio';\n}\n\nexport function isCaptionClip(clip: Clip): clip is CaptionClip {\n return clip.trackKind === 'caption';\n}\n\nexport function isFxClip(clip: Clip): clip is FxClip {\n return clip.trackKind === 'fx';\n}\n\nexport function hasResourceId(clip: Clip): clip is VideoClip | AudioClip {\n return isVideoClip(clip) || isAudioClip(clip);\n}\n\nexport function hasAudioConfig(clip: Clip): clip is VideoClip | AudioClip {\n return isVideoClip(clip) || isAudioClip(clip);\n}\n"],"names":[],"mappings":"AAyTO,SAAS,YAAY,MAA+B;AACzD,SAAO,KAAK,cAAc;AAC5B;AAEO,SAAS,YAAY,MAA+B;AACzD,SAAO,KAAK,cAAc;AAC5B;AAUO,SAAS,cAAc,MAA2C;AACvE,SAAO,YAAY,IAAI,KAAK,YAAY,IAAI;AAC9C;AAEO,SAAS,eAAe,MAA2C;AACxE,SAAO,YAAY,IAAI,KAAK,YAAY,IAAI;AAC9C;"}
@@ -1 +1 @@
1
- {"version":3,"file":"AudioWindowPreparer.d.ts","sourceRoot":"","sources":["../../src/orchestrator/AudioWindowPreparer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,uBAAuB;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,KAAK,CAAiC;gBAElC,IAAI,EAAE,uBAAuB;IAIzC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIvC,QAAQ,IAAI,gBAAgB,GAAG,IAAI;IAInC,KAAK,IAAI,IAAI;IAKb,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAM5C;;;;;;;OAOG;IACH,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAmB7D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CtC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnD;;OAEG;IACG,uBAAuB,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5E,OAAO,CAAC,IAAI,CAAC;IAwEhB;;OAEG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IAShB;;OAEG;YACW,iBAAiB;IA8E/B;;OAEG;YACW,kBAAkB;CAiEjC"}
1
+ {"version":3,"file":"AudioWindowPreparer.d.ts","sourceRoot":"","sources":["../../src/orchestrator/AudioWindowPreparer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG3C,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,uBAAuB;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,KAAK,CAAiC;gBAElC,IAAI,EAAE,uBAAuB;IAIzC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIvC,QAAQ,IAAI,gBAAgB,GAAG,IAAI;IAInC,KAAK,IAAI,IAAI;IAKb,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAM5C;;;;;;;OAOG;IACH,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAmB7D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CtC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnD;;OAEG;IACG,uBAAuB,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5E,OAAO,CAAC,IAAI,CAAC;IA+FhB;;OAEG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IAShB;;OAEG;YACW,iBAAiB;IA8E/B;;OAEG;YACW,kBAAkB;CAiEjC"}
@@ -1,6 +1,7 @@
1
1
  import { MeframeEvent } from "../event/events.js";
2
2
  import { AudioChunkDecoder } from "../stages/decode/AudioChunkDecoder.js";
3
3
  import { hasResourceId, isAudioClip } from "../model/types.js";
4
+ import { buildLoopedResourceSegments } from "../utils/loop-utils.js";
4
5
  class AudioWindowPreparer {
5
6
  activeClips = /* @__PURE__ */ new Set();
6
7
  deps;
@@ -118,9 +119,28 @@ class AudioWindowPreparer {
118
119
  const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);
119
120
  const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);
120
121
  const trimStartUs = clip.trimStartUs ?? 0;
121
- const resourceStartUs = clipRelativeStartUs + trimStartUs;
122
- const resourceEndUs = clipRelativeEndUs + trimStartUs;
123
- await this.ensureAudioWindow(clip.id, resourceStartUs, resourceEndUs, strictMode);
122
+ const loop = clip.trackKind === "audio" && clip.loop === true;
123
+ const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);
124
+ const resourceDurationUs = audioRecord?.durationUs ?? 0;
125
+ let segments = buildLoopedResourceSegments({
126
+ clipRelativeStartUs,
127
+ clipRelativeEndUs,
128
+ trimStartUs,
129
+ resourceDurationUs,
130
+ loop
131
+ });
132
+ if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {
133
+ segments = buildLoopedResourceSegments({
134
+ clipRelativeStartUs,
135
+ clipRelativeEndUs,
136
+ trimStartUs,
137
+ resourceDurationUs,
138
+ loop: false
139
+ });
140
+ }
141
+ for (const seg of segments) {
142
+ await this.ensureAudioWindow(clip.id, seg.resourceStartUs, seg.resourceEndUs, strictMode);
143
+ }
124
144
  });
125
145
  if (mode === "probe") {
126
146
  void Promise.all(ensurePromises);
@@ -1 +1 @@
1
- {"version":3,"file":"AudioWindowPreparer.js","sources":["../../src/orchestrator/AudioWindowPreparer.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport type { CompositionModel } from '../model';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { MeframeEvent } from '../event/events';\nimport type { CacheManager } from '../cache/CacheManager';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } from '../model/types';\nimport type { RequestMode } from './types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioWindowPreparerDeps {\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n}\n\nexport class AudioWindowPreparer {\n private activeClips = new Set<string>();\n private deps: AudioWindowPreparerDeps;\n private model: CompositionModel | null = null;\n\n constructor(deps: AudioWindowPreparerDeps) {\n this.deps = deps;\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n }\n\n getModel(): CompositionModel | null {\n return this.model;\n }\n\n reset(): void {\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n /**\n * Fast readiness probe for preview playback.\n *\n * This is intentionally synchronous and lightweight:\n * - Only checks resource-level readiness (download + MP4 index parsing).\n * - If any relevant resource isn't ready yet, return false.\n * - Does NOT require audio samples / PCM window coverage (probe is resource-level only).\n */\n isAudioResourceWindowReady(startUs: TimeUs, endUs: TimeUs): boolean {\n const model = this.model;\n if (!model) return true;\n\n const activeClips = model.getActiveClips(startUs, endUs);\n\n for (const clip of activeClips) {\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') continue;\n if (!hasResourceId(clip)) continue;\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n return false;\n }\n }\n\n return true;\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n /**\n * Core method to ensure audio for all clips in a time range\n */\n async ensureAudioForTimeRange(\n startUs: TimeUs,\n endUs: TimeUs,\n options: { mode?: RequestMode; loadResource?: boolean; strictMode?: boolean }\n ): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const { mode = 'blocking', loadResource = true, strictMode = false } = options;\n\n // Find all clips that overlap with [startUs, endUs]\n const activeClips = model.getActiveClips(startUs, endUs);\n\n const ensurePromises = activeClips.map(async (clip) => {\n // Only process audio and video clips.\n // Note: video clips may carry an embedded audio track; preview should include them as well.\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') return;\n if (!hasResourceId(clip)) return;\n\n // Skip muted/zero-volume clips early; they should not block preview or export.\n const muted = clip.audioConfig?.muted ?? false;\n const volume = clip.audioConfig?.volume ?? 1.0;\n if (muted || volume <= 0) {\n return;\n }\n\n // Skip clips without audio track (performance optimization)\n // If AudioSampleCache doesn't have this resource, and resource is ready,\n // it means the resource has no audio track (e.g., video-only or image)\n const resource = model.getResource(clip.resourceId);\n if (\n resource?.state === 'ready' &&\n !this.deps.cacheManager.audioSampleCache.has(clip.resourceId)\n ) {\n // Resource is ready but has no audio samples - skip\n return;\n }\n\n // Ensure AudioSampleCache has data\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n if (!loadResource) {\n // Export mode: skip clips without cached samples\n return;\n }\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n // Resource not yet loaded - wait for it\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n }\n\n // Calculate clip-relative time range\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Convert to resource time (aligned with video architecture)\n const trimStartUs = clip.trimStartUs ?? 0;\n const resourceStartUs = clipRelativeStartUs + trimStartUs;\n const resourceEndUs = clipRelativeEndUs + trimStartUs;\n\n // Ensure audio window using resource time coordinates\n await this.ensureAudioWindow(clip.id, resourceStartUs, resourceEndUs, strictMode);\n });\n\n if (mode === 'probe') {\n void Promise.all(ensurePromises);\n return;\n }\n await Promise.all(ensurePromises);\n }\n\n /**\n * Ensure audio window for a clip\n */\n async ensureAudioWindow(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): Promise<void> {\n // Check L1 cache - window-level verification\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs, strictMode)) {\n return; // Entire window has sufficient data\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Incremental decoding with smart threshold strategy\n const INCREMENTAL_THRESHOLD = 0.95; // 95% coverage: skip decode\n const FULL_FALLBACK_THRESHOLD = 0.3; // <30% coverage: full decode to avoid fragmentation\n\n // Check window-level coverage\n const coverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n startUs,\n endUs,\n INCREMENTAL_THRESHOLD\n );\n\n // Strategy 1: High coverage - skip decode entirely\n if (coverage.covered) {\n return;\n }\n\n // Strategy 2: Very low coverage - full decode (avoid fragmentation overhead)\n if (coverage.coverageRatio < FULL_FALLBACK_THRESHOLD) {\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n return;\n }\n\n // Strategy 3: Medium coverage - incremental decode (30%-95%)\n const chunksToDecode = windowChunks.filter((chunk) => {\n const chunkEndUs = chunk.timestamp + (chunk.duration ?? 0);\n const chunkCoverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n chunk.timestamp,\n chunkEndUs,\n 0.95\n );\n return !chunkCoverage.covered;\n });\n\n if (chunksToDecode.length === 0) {\n return;\n }\n\n // Decode only missing chunks\n await this.decodeAudioSamples(\n clipId,\n chunksToDecode,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n const view = config.description as Uint8Array;\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[AudioWindowPreparer] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\n }\n}\n"],"names":["resource"],"mappings":"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"}
1
+ {"version":3,"file":"AudioWindowPreparer.js","sources":["../../src/orchestrator/AudioWindowPreparer.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport type { CompositionModel } from '../model';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { MeframeEvent } from '../event/events';\nimport type { CacheManager } from '../cache/CacheManager';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } from '../model/types';\nimport type { RequestMode } from './types';\nimport { buildLoopedResourceSegments } from '../utils/loop-utils';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioWindowPreparerDeps {\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n}\n\nexport class AudioWindowPreparer {\n private activeClips = new Set<string>();\n private deps: AudioWindowPreparerDeps;\n private model: CompositionModel | null = null;\n\n constructor(deps: AudioWindowPreparerDeps) {\n this.deps = deps;\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n }\n\n getModel(): CompositionModel | null {\n return this.model;\n }\n\n reset(): void {\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n /**\n * Fast readiness probe for preview playback.\n *\n * This is intentionally synchronous and lightweight:\n * - Only checks resource-level readiness (download + MP4 index parsing).\n * - If any relevant resource isn't ready yet, return false.\n * - Does NOT require audio samples / PCM window coverage (probe is resource-level only).\n */\n isAudioResourceWindowReady(startUs: TimeUs, endUs: TimeUs): boolean {\n const model = this.model;\n if (!model) return true;\n\n const activeClips = model.getActiveClips(startUs, endUs);\n\n for (const clip of activeClips) {\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') continue;\n if (!hasResourceId(clip)) continue;\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n return false;\n }\n }\n\n return true;\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n /**\n * Core method to ensure audio for all clips in a time range\n */\n async ensureAudioForTimeRange(\n startUs: TimeUs,\n endUs: TimeUs,\n options: { mode?: RequestMode; loadResource?: boolean; strictMode?: boolean }\n ): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const { mode = 'blocking', loadResource = true, strictMode = false } = options;\n\n // Find all clips that overlap with [startUs, endUs]\n const activeClips = model.getActiveClips(startUs, endUs);\n\n const ensurePromises = activeClips.map(async (clip) => {\n // Only process audio and video clips.\n // Note: video clips may carry an embedded audio track; preview should include them as well.\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') return;\n if (!hasResourceId(clip)) return;\n\n // Skip muted/zero-volume clips early; they should not block preview or export.\n const muted = clip.audioConfig?.muted ?? false;\n const volume = clip.audioConfig?.volume ?? 1.0;\n if (muted || volume <= 0) {\n return;\n }\n\n // Skip clips without audio track (performance optimization)\n // If AudioSampleCache doesn't have this resource, and resource is ready,\n // it means the resource has no audio track (e.g., video-only or image)\n const resource = model.getResource(clip.resourceId);\n if (\n resource?.state === 'ready' &&\n !this.deps.cacheManager.audioSampleCache.has(clip.resourceId)\n ) {\n // Resource is ready but has no audio samples - skip\n return;\n }\n\n // Ensure AudioSampleCache has data\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n if (!loadResource) {\n // Export mode: skip clips without cached samples\n return;\n }\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n // Resource not yet loaded - wait for it\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n }\n\n // Calculate clip-relative time range\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Convert to resource time (aligned with video architecture)\n const trimStartUs = clip.trimStartUs ?? 0;\n const loop = clip.trackKind === 'audio' && clip.loop === true;\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId) as {\n durationUs: TimeUs;\n } | null;\n const resourceDurationUs = audioRecord?.durationUs ?? 0;\n\n let segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop,\n });\n if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {\n // Fallback to non-loop mapping when resource duration isn't available yet.\n segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop: false,\n });\n }\n\n // Ensure audio window using resource time coordinates\n for (const seg of segments) {\n await this.ensureAudioWindow(clip.id, seg.resourceStartUs, seg.resourceEndUs, strictMode);\n }\n });\n\n if (mode === 'probe') {\n void Promise.all(ensurePromises);\n return;\n }\n await Promise.all(ensurePromises);\n }\n\n /**\n * Ensure audio window for a clip\n */\n async ensureAudioWindow(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): Promise<void> {\n // Check L1 cache - window-level verification\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs, strictMode)) {\n return; // Entire window has sufficient data\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Incremental decoding with smart threshold strategy\n const INCREMENTAL_THRESHOLD = 0.95; // 95% coverage: skip decode\n const FULL_FALLBACK_THRESHOLD = 0.3; // <30% coverage: full decode to avoid fragmentation\n\n // Check window-level coverage\n const coverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n startUs,\n endUs,\n INCREMENTAL_THRESHOLD\n );\n\n // Strategy 1: High coverage - skip decode entirely\n if (coverage.covered) {\n return;\n }\n\n // Strategy 2: Very low coverage - full decode (avoid fragmentation overhead)\n if (coverage.coverageRatio < FULL_FALLBACK_THRESHOLD) {\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n return;\n }\n\n // Strategy 3: Medium coverage - incremental decode (30%-95%)\n const chunksToDecode = windowChunks.filter((chunk) => {\n const chunkEndUs = chunk.timestamp + (chunk.duration ?? 0);\n const chunkCoverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n chunk.timestamp,\n chunkEndUs,\n 0.95\n );\n return !chunkCoverage.covered;\n });\n\n if (chunksToDecode.length === 0) {\n return;\n }\n\n // Decode only missing chunks\n await this.decodeAudioSamples(\n clipId,\n chunksToDecode,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n const view = config.description as Uint8Array;\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[AudioWindowPreparer] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\n }\n}\n"],"names":["resource"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineAudioMixer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAS7D,qBAAa,iBAAiB;IAK1B,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,QAAQ;IALlB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,gBAAgB,CAAK;gBAGnB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI;IAG3C,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAyF3E,OAAO,CAAC,gBAAgB;CA2CzB"}
1
+ {"version":3,"file":"OfflineAudioMixer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAAa,iBAAiB;IAK1B,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,QAAQ;IALlB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,gBAAgB,CAAK;gBAGnB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI;IAG3C,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAyG3E,OAAO,CAAC,gBAAgB;CA2CzB"}
@@ -1,4 +1,5 @@
1
- import { hasAudioConfig } from "../../model/types.js";
1
+ import { hasResourceId, hasAudioConfig } from "../../model/types.js";
2
+ import { buildLoopedResourceSegments } from "../../utils/loop-utils.js";
2
3
  class OfflineAudioMixer {
3
4
  constructor(cacheManager, getModel) {
4
5
  this.cacheManager = cacheManager;
@@ -29,36 +30,54 @@ class OfflineAudioMixer {
29
30
  const clipRelativeEndUs = clipIntersectEndUs - clip.startUs;
30
31
  const clipModel = this.getModel()?.findClip(clip.clipId);
31
32
  const trimStartUs = clipModel?.trimStartUs ?? 0;
32
- const resourceStartUs = clipRelativeStartUs + trimStartUs;
33
- const resourceEndUs = clipRelativeEndUs + trimStartUs;
34
- const pcmData = this.cacheManager.getClipPCMWithMetadata(
35
- clip.clipId,
36
- resourceStartUs,
37
- resourceEndUs
38
- );
39
- if (!pcmData || pcmData.planes.length === 0) {
40
- continue;
33
+ const loop = clipModel?.trackKind === "audio" && clipModel.loop === true;
34
+ const resourceDurationUs = clipModel && hasResourceId(clipModel) ? this.cacheManager.audioSampleCache.get(clipModel.resourceId)?.durationUs ?? 0 : 0;
35
+ let segments = buildLoopedResourceSegments({
36
+ clipRelativeStartUs,
37
+ clipRelativeEndUs,
38
+ trimStartUs,
39
+ resourceDurationUs,
40
+ loop
41
+ });
42
+ if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {
43
+ segments = buildLoopedResourceSegments({
44
+ clipRelativeStartUs,
45
+ clipRelativeEndUs,
46
+ trimStartUs,
47
+ resourceDurationUs,
48
+ loop: false
49
+ });
41
50
  }
42
- const intersectFrames = pcmData.planes[0]?.length ?? 0;
43
- if (intersectFrames === 0) {
44
- continue;
45
- }
46
- const buffer = ctx.createBuffer(pcmData.planes.length, intersectFrames, pcmData.sampleRate);
47
- for (let channel = 0; channel < pcmData.planes.length; channel++) {
48
- const plane = pcmData.planes[channel];
49
- if (plane) {
50
- buffer.copyToChannel(new Float32Array(plane), channel);
51
+ for (const seg of segments) {
52
+ const pcmData = this.cacheManager.getClipPCMWithMetadata(
53
+ clip.clipId,
54
+ seg.resourceStartUs,
55
+ seg.resourceEndUs
56
+ );
57
+ if (!pcmData || pcmData.planes.length === 0) {
58
+ continue;
59
+ }
60
+ const intersectFrames = pcmData.planes[0]?.length ?? 0;
61
+ if (intersectFrames === 0) {
62
+ continue;
63
+ }
64
+ const buffer = ctx.createBuffer(pcmData.planes.length, intersectFrames, pcmData.sampleRate);
65
+ for (let channel = 0; channel < pcmData.planes.length; channel++) {
66
+ const plane = pcmData.planes[channel];
67
+ if (plane) {
68
+ buffer.copyToChannel(new Float32Array(plane), channel);
69
+ }
51
70
  }
71
+ const source = ctx.createBufferSource();
72
+ source.buffer = buffer;
73
+ const gainNode = ctx.createGain();
74
+ gainNode.gain.value = clip.volume;
75
+ source.connect(gainNode);
76
+ gainNode.connect(ctx.destination);
77
+ const segmentStartUs = clip.startUs + seg.clipRelativeStartUs;
78
+ const startTime = (segmentStartUs - windowStartUs) / 1e6;
79
+ source.start(startTime);
52
80
  }
53
- const source = ctx.createBufferSource();
54
- source.buffer = buffer;
55
- const gainNode = ctx.createGain();
56
- gainNode.gain.value = clip.volume;
57
- source.connect(gainNode);
58
- gainNode.connect(ctx.destination);
59
- const relativeStartUs = clipIntersectStartUs - windowStartUs;
60
- const startTime = relativeStartUs / 1e6;
61
- source.start(startTime);
62
81
  }
63
82
  const mixedBuffer = await ctx.startRendering();
64
83
  return mixedBuffer;
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineAudioMixer.js","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { hasAudioConfig } from '../../model/types';\nimport type { CompositionModel } from '../../model';\nimport type { CacheManager } from '../../cache/CacheManager';\n\ninterface MixClipInfo {\n clipId: string;\n startUs: TimeUs;\n durationUs: TimeUs;\n volume: number;\n}\n\nexport class OfflineAudioMixer {\n private sampleRate = 48_000;\n private numberOfChannels = 2;\n\n constructor(\n private cacheManager: CacheManager,\n private getModel: () => CompositionModel | null\n ) {}\n\n async mix(windowStartUs: TimeUs, windowEndUs: TimeUs): Promise<AudioBuffer> {\n const durationUs = windowEndUs - windowStartUs;\n // Guard against invalid/empty ranges (can happen near timeline end or after clamping).\n // OfflineAudioContext requires length >= 1.\n const frameCount = Math.max(\n 1,\n Math.ceil((Math.max(0, durationUs) / 1_000_000) * this.sampleRate)\n );\n\n const ctx = new OfflineAudioContext(this.numberOfChannels, frameCount, this.sampleRate);\n\n // Ensure the OfflineAudioContext renders the full requested length.\n // Some implementations may stop early if no sources are scheduled near the tail,\n // which would truncate trailing silence and make export audio shorter than video.\n const silent = ctx.createBuffer(1, frameCount, this.sampleRate);\n const silentSource = ctx.createBufferSource();\n silentSource.buffer = silent;\n const silentGain = ctx.createGain();\n silentGain.gain.value = 0;\n silentSource.connect(silentGain);\n silentGain.connect(ctx.destination);\n silentSource.start(0);\n\n const clips = this.getClipsInWindow(windowStartUs, windowEndUs);\n\n for (const clip of clips) {\n // Calculate clip-relative time range\n const clipIntersectStartUs = Math.max(windowStartUs, clip.startUs);\n const clipIntersectEndUs = Math.min(windowEndUs, clip.startUs + clip.durationUs);\n const clipRelativeStartUs = clipIntersectStartUs - clip.startUs;\n const clipRelativeEndUs = clipIntersectEndUs - clip.startUs;\n\n // Convert to resource time (aligned with video architecture)\n const clipModel = this.getModel()?.findClip(clip.clipId);\n const trimStartUs = clipModel?.trimStartUs ?? 0;\n const resourceStartUs = clipRelativeStartUs + trimStartUs;\n const resourceEndUs = clipRelativeEndUs + trimStartUs;\n\n // Get PCM data using resource time coordinates\n const pcmData = this.cacheManager.getClipPCMWithMetadata(\n clip.clipId,\n resourceStartUs,\n resourceEndUs\n );\n\n if (!pcmData || pcmData.planes.length === 0) {\n // console.warn(\n // `[OfflineAudioMixer] No PCM data for clip ${clip.clipId} at ${(clipRelativeStartUs / 1000).toFixed(1)}-${(clipRelativeEndUs / 1000).toFixed(1)}ms`\n // );\n continue;\n }\n\n const intersectFrames = pcmData.planes[0]?.length ?? 0;\n if (intersectFrames === 0) {\n // console.warn(\n // `[OfflineAudioMixer] Empty PCM data for clip ${clip.clipId} at ${(clipRelativeStartUs / 1000).toFixed(1)}-${(clipRelativeEndUs / 1000).toFixed(1)}ms`\n // );\n continue;\n }\n\n // Create AudioBuffer\n const buffer = ctx.createBuffer(pcmData.planes.length, intersectFrames, pcmData.sampleRate);\n\n for (let channel = 0; channel < pcmData.planes.length; channel++) {\n const plane = pcmData.planes[channel];\n if (plane) {\n // Create new Float32Array to ensure correct type (ArrayBuffer, not SharedArrayBuffer)\n buffer.copyToChannel(new Float32Array(plane), channel);\n }\n }\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n\n const gainNode = ctx.createGain();\n gainNode.gain.value = clip.volume;\n\n source.connect(gainNode);\n gainNode.connect(ctx.destination);\n\n const relativeStartUs = clipIntersectStartUs - windowStartUs;\n const startTime = relativeStartUs / 1_000_000;\n source.start(startTime);\n }\n\n const mixedBuffer = await ctx.startRendering();\n return mixedBuffer;\n }\n\n private getClipsInWindow(windowStartUs: TimeUs, windowEndUs: TimeUs): MixClipInfo[] {\n const clips: MixClipInfo[] = [];\n const model = this.getModel();\n if (!model) {\n return clips;\n }\n\n for (const track of model.tracks) {\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n if (clip.startUs < windowEndUs && clipEndUs > windowStartUs) {\n // Read audio config (only video/audio clips have audioConfig)\n if (hasAudioConfig(clip)) {\n const muted = clip.audioConfig?.muted ?? false;\n\n // Skip muted clips in export (performance optimization)\n if (muted) {\n continue;\n }\n\n const volume = clip.audioConfig?.volume ?? 1.0;\n\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume,\n });\n } else {\n // Caption/Fx clips in audio track should not happen, but handle gracefully\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume: 1.0,\n });\n }\n }\n }\n }\n\n return clips;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OfflineAudioMixer.js","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { hasAudioConfig, hasResourceId } from '../../model/types';\nimport type { CompositionModel } from '../../model';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport { buildLoopedResourceSegments } from '../../utils/loop-utils';\n\ninterface MixClipInfo {\n clipId: string;\n startUs: TimeUs;\n durationUs: TimeUs;\n volume: number;\n}\n\nexport class OfflineAudioMixer {\n private sampleRate = 48_000;\n private numberOfChannels = 2;\n\n constructor(\n private cacheManager: CacheManager,\n private getModel: () => CompositionModel | null\n ) {}\n\n async mix(windowStartUs: TimeUs, windowEndUs: TimeUs): Promise<AudioBuffer> {\n const durationUs = windowEndUs - windowStartUs;\n // Guard against invalid/empty ranges (can happen near timeline end or after clamping).\n // OfflineAudioContext requires length >= 1.\n const frameCount = Math.max(\n 1,\n Math.ceil((Math.max(0, durationUs) / 1_000_000) * this.sampleRate)\n );\n\n const ctx = new OfflineAudioContext(this.numberOfChannels, frameCount, this.sampleRate);\n\n // Ensure the OfflineAudioContext renders the full requested length.\n // Some implementations may stop early if no sources are scheduled near the tail,\n // which would truncate trailing silence and make export audio shorter than video.\n const silent = ctx.createBuffer(1, frameCount, this.sampleRate);\n const silentSource = ctx.createBufferSource();\n silentSource.buffer = silent;\n const silentGain = ctx.createGain();\n silentGain.gain.value = 0;\n silentSource.connect(silentGain);\n silentGain.connect(ctx.destination);\n silentSource.start(0);\n\n const clips = this.getClipsInWindow(windowStartUs, windowEndUs);\n\n for (const clip of clips) {\n // Calculate clip-relative time range\n const clipIntersectStartUs = Math.max(windowStartUs, clip.startUs);\n const clipIntersectEndUs = Math.min(windowEndUs, clip.startUs + clip.durationUs);\n const clipRelativeStartUs = clipIntersectStartUs - clip.startUs;\n const clipRelativeEndUs = clipIntersectEndUs - clip.startUs;\n\n // Convert to resource time (aligned with video architecture)\n const clipModel = this.getModel()?.findClip(clip.clipId);\n const trimStartUs = clipModel?.trimStartUs ?? 0;\n const loop = clipModel?.trackKind === 'audio' && clipModel.loop === true;\n const resourceDurationUs =\n clipModel && hasResourceId(clipModel)\n ? (this.cacheManager.audioSampleCache.get(clipModel.resourceId)?.durationUs ?? 0)\n : 0;\n\n let segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop,\n });\n if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {\n segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop: false,\n });\n }\n\n for (const seg of segments) {\n // Get PCM data using resource time coordinates\n const pcmData = this.cacheManager.getClipPCMWithMetadata(\n clip.clipId,\n seg.resourceStartUs,\n seg.resourceEndUs\n );\n\n if (!pcmData || pcmData.planes.length === 0) {\n continue;\n }\n\n const intersectFrames = pcmData.planes[0]?.length ?? 0;\n if (intersectFrames === 0) {\n continue;\n }\n\n // Create AudioBuffer\n const buffer = ctx.createBuffer(pcmData.planes.length, intersectFrames, pcmData.sampleRate);\n\n for (let channel = 0; channel < pcmData.planes.length; channel++) {\n const plane = pcmData.planes[channel];\n if (plane) {\n // Create new Float32Array to ensure correct type (ArrayBuffer, not SharedArrayBuffer)\n buffer.copyToChannel(new Float32Array(plane), channel);\n }\n }\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n\n const gainNode = ctx.createGain();\n gainNode.gain.value = clip.volume;\n\n source.connect(gainNode);\n gainNode.connect(ctx.destination);\n\n const segmentStartUs = clip.startUs + seg.clipRelativeStartUs;\n const startTime = (segmentStartUs - windowStartUs) / 1_000_000;\n source.start(startTime);\n }\n }\n\n const mixedBuffer = await ctx.startRendering();\n return mixedBuffer;\n }\n\n private getClipsInWindow(windowStartUs: TimeUs, windowEndUs: TimeUs): MixClipInfo[] {\n const clips: MixClipInfo[] = [];\n const model = this.getModel();\n if (!model) {\n return clips;\n }\n\n for (const track of model.tracks) {\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n if (clip.startUs < windowEndUs && clipEndUs > windowStartUs) {\n // Read audio config (only video/audio clips have audioConfig)\n if (hasAudioConfig(clip)) {\n const muted = clip.audioConfig?.muted ?? false;\n\n // Skip muted clips in export (performance optimization)\n if (muted) {\n continue;\n }\n\n const volume = clip.audioConfig?.volume ?? 1.0;\n\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume,\n });\n } else {\n // Caption/Fx clips in audio track should not happen, but handle gracefully\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume: 1.0,\n });\n }\n }\n }\n }\n\n return clips;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MP4IndexParser.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,SAAS,CAAC;AAKvF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,QAAQ,CAAC;IAChB,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACnC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,KAAK,IAAI,CAAC;CACpF;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;IACzB;;;;OAIG;IACG,eAAe,CACnB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,cAAc,CAAC;YAiLZ,iBAAiB;IA2C/B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,oBAAoB;IAqC5B;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiChE;;OAEG;IACH,OAAO,CAAC,UAAU;IAoBlB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiB5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAwCxB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAsCrB;;;OAGG;IACH,OAAO,CAAC,eAAe;CAqExB"}
1
+ {"version":3,"file":"MP4IndexParser.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,SAAS,CAAC;AAKvF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,QAAQ,CAAC;IAChB,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACnC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,KAAK,IAAI,CAAC;CACpF;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;IACzB;;;;OAIG;IACG,eAAe,CACnB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,cAAc,CAAC;YAwLZ,iBAAiB;IA0C/B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,oBAAoB;IAsC5B;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiChE;;OAEG;IACH,OAAO,CAAC,UAAU;IAoBlB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiB5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAwCxB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAsCrB;;;OAGG;IACH,OAAO,CAAC,eAAe;CAqExB"}
@@ -59,6 +59,9 @@ class MP4IndexParser {
59
59
  audioTimescale = timescale;
60
60
  expectedAudioSamples = expectedSamples;
61
61
  streamState.audioComplete = isComplete;
62
+ if (!isComplete && audioTrackId && audioConfig) {
63
+ mp4boxFile.start();
64
+ }
62
65
  if (isComplete) {
63
66
  setTimeout(() => {
64
67
  if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {
@@ -214,11 +217,10 @@ class MP4IndexParser {
214
217
  sampleRate: audioTrack.audio.sample_rate || audioTrack.timescale,
215
218
  numberOfChannels: audioTrack.audio.channel_count
216
219
  };
217
- mp4boxFile.setExtractionOptions(trackId, null, {
220
+ mp4boxFile.setExtractionOptions(trackId, audioTrack, {
218
221
  nbSamples: Infinity
219
222
  // Extract all samples
220
223
  });
221
- mp4boxFile.start();
222
224
  return {
223
225
  config,
224
226
  trackId,
@@ -1 +1 @@
1
- {"version":3,"file":"MP4IndexParser.js","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"sourcesContent":["import { MP4Box } from '../../utils/mp4box';\nimport type { MP4BoxFile } from 'mp4box';\nimport type { MP4Index, VideoTrackIndex, AudioTrackIndex, Sample, GOP } from './types';\nimport type { TimeUs } from '../../model/types';\nimport { MP4Demuxer, normalizeVideoCodec } from './MP4Demuxer';\nimport { EmptyStreamError } from '../../utils/errors';\n\nexport interface MP4ParseResult {\n index: MP4Index;\n audioSamples?: EncodedAudioChunk[];\n audioMetadata?: AudioDecoderConfig;\n}\n\nexport interface ParseStreamOptions {\n resourceId?: string;\n onFirstFrameReady?: (index: MP4Index, firstGOPChunks: EncodedVideoChunk[]) => void;\n}\n\n/**\n * MP4IndexParser - Parse MP4 moov box and build time→byte index\n *\n * Features:\n * - Stream-based moov parsing (stop after moov found)\n * - Build sample table with byte offsets\n * - Build GOP index for keyframe positions\n * - Extract all audio samples (encoded) for memory caching\n * - Extract first GOP for fast cover rendering\n */\nexport class MP4IndexParser {\n /**\n * Parse from streaming download\n * Returns video index + all audio samples\n * Can optionally extract first GOP for fast cover rendering\n */\n async parseFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: ParseStreamOptions\n ): Promise<MP4ParseResult> {\n const resourceId = options?.resourceId || 'unknown';\n const mp4boxFile = MP4Box.createFile();\n let resolveResult: ((result: MP4ParseResult) => void) | null = null;\n let rejectResult: ((error: Error) => void) | null = null;\n\n const audioChunks: EncodedAudioChunk[] = [];\n let audioConfig: AudioDecoderConfig | undefined;\n let audioTrackId: number | undefined;\n let audioTimescale: number | undefined;\n let expectedAudioSamples = 0;\n let savedInfo: any = null;\n\n const resultPromise = new Promise<MP4ParseResult>((resolve, reject) => {\n resolveResult = resolve;\n rejectResult = reject;\n });\n\n // First GOP extraction state\n const firstGOPState = {\n byteEnd: 0,\n extracted: !options?.onFirstFrameReady,\n index: null as MP4Index | null,\n buffer: new Uint8Array(0),\n };\n\n const streamState = {\n fileOffset: 0,\n moovParsed: false,\n audioComplete: false,\n };\n\n mp4boxFile.onError = (error: string) => {\n rejectResult?.(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n // Set moovParsed immediately\n streamState.moovParsed = true;\n\n // Save info for later use in audio extraction\n savedInfo = info;\n\n // Build video index\n const index = this.buildIndex(mp4boxFile, info);\n firstGOPState.index = index;\n\n // Check if this is a fast-start format (moov at beginning)\n const isFastStart = info.isProgressive === true;\n\n // Only extract first GOP for fast-start videos (moov-at-end videos will read from OPFS)\n if (isFastStart && options?.onFirstFrameReady && index.tracks.video?.gopIndex[0]) {\n const firstGOP = index.tracks.video.gopIndex[0];\n const samples = index.tracks.video.samples;\n const startIdx = firstGOP.keyframeSampleIndex;\n const endIdx = startIdx + firstGOP.sampleCount;\n\n const endSample = samples[endIdx - 1];\n if (endSample) {\n firstGOPState.byteEnd = endSample.byteOffset + endSample.byteLength;\n }\n }\n\n // Setup audio extraction if audio track exists\n const { config, trackId, timescale, expectedSamples, isComplete } =\n this.setupAudioExtraction(mp4boxFile, info);\n audioConfig = config;\n audioTrackId = trackId;\n audioTimescale = timescale;\n expectedAudioSamples = expectedSamples;\n streamState.audioComplete = isComplete;\n\n if (isComplete) {\n // No audio track, resolve immediately if firstGOP extracted or not needed\n // Use setTimeout to allow stack to unwind and ensure consistency\n setTimeout(() => {\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n resolveResult?.({ index });\n }\n }, 0);\n }\n } catch (error) {\n rejectResult?.(error as Error);\n }\n };\n\n // Handle audio sample extraction\n mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n if (trackId === audioTrackId && audioConfig) {\n // Use track timescale, fallback to sampleRate if timescale not available\n // MP4 sample.cts and sample.duration are in track timescale units\n const timescale = audioTimescale || audioConfig.sampleRate;\n\n // Process samples if any\n if (samples && samples.length > 0) {\n for (const sample of samples) {\n try {\n const chunk = new EncodedAudioChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp: Math.round((sample.cts / timescale) * 1_000_000),\n duration: Math.round((sample.duration / timescale) * 1_000_000),\n data: sample.data,\n });\n audioChunks.push(chunk);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create audio chunk:', error);\n }\n }\n }\n\n // Check if we have all samples\n if (expectedAudioSamples !== Infinity && audioChunks.length >= expectedAudioSamples) {\n streamState.audioComplete = true;\n }\n }\n };\n\n await this.processStreamData(\n stream,\n mp4boxFile,\n options,\n firstGOPState,\n streamState,\n resourceId\n );\n\n // Flush remaining data\n mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onReady/onSamples are async)\n // Reference: MP4Demuxer.ts line 302\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // If audio extraction finished normally (or there was no audio), resolve now\n if (streamState.moovParsed && streamState.audioComplete) {\n const index = firstGOPState.index || this.buildIndex(mp4boxFile, savedInfo);\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n }\n // If moov is parsed but audio extraction hasn't completed yet, force complete\n else if (streamState.moovParsed && !streamState.audioComplete && savedInfo) {\n streamState.audioComplete = true;\n const index = this.buildIndex(mp4boxFile, savedInfo);\n\n // Check if we have audio chunks\n if (audioChunks.length > 0 && audioConfig) {\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n } else {\n // No audio or extraction failed, just return index\n resolveResult!({ index });\n }\n }\n\n // Wait for result with timeout (5s max)\n const parseTimeout = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `MP4Box parsing timeout after reading ${streamState.fileOffset} bytes. ` +\n `moovParsed=${streamState.moovParsed}, audioExtractionComplete=${streamState.audioComplete}`\n )\n );\n }, 5000);\n });\n\n // Wait for either resultPromise or timeout\n return await Promise.race([resultPromise, parseTimeout]);\n }\n\n private async processStreamData(\n stream: ReadableStream<Uint8Array>,\n mp4boxFile: MP4BoxFile,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any,\n resourceId: string\n ): Promise<void> {\n const reader = stream.getReader();\n\n try {\n let hasData = false;\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n hasData = true;\n const buffer = this.prepareBuffer(value, streamState.fileOffset);\n\n this.handleFirstGOPExtraction(buffer, value, options, firstGOPState, streamState);\n\n mp4boxFile.appendBuffer(buffer);\n streamState.fileOffset += buffer.byteLength;\n\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n break;\n }\n }\n }\n\n if (!hasData) {\n throw new EmptyStreamError(resourceId, streamState.fileOffset);\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private prepareBuffer(value: Uint8Array, fileOffset: number): ArrayBuffer {\n let buffer: ArrayBuffer;\n if (value.byteOffset === 0 && value.byteLength === value.buffer.byteLength) {\n buffer = value.buffer as ArrayBuffer;\n } else {\n buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n ) as ArrayBuffer;\n }\n (buffer as any).fileStart = fileOffset;\n return buffer;\n }\n\n private handleFirstGOPExtraction(\n buffer: ArrayBuffer,\n value: Uint8Array,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): void {\n if (!firstGOPState.extracted && options?.onFirstFrameReady) {\n // Safety: Cap accumulation to 10MB\n if (firstGOPState.buffer.length + value.length < 10 * 1024 * 1024) {\n const newBuffer = new Uint8Array(firstGOPState.buffer.length + value.length);\n newBuffer.set(firstGOPState.buffer);\n newBuffer.set(value, firstGOPState.buffer.length);\n firstGOPState.buffer = newBuffer;\n } else if (!streamState.moovParsed) {\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n\n if (\n streamState.moovParsed &&\n firstGOPState.byteEnd > 0 &&\n firstGOPState.index &&\n streamState.fileOffset + buffer.byteLength >= firstGOPState.byteEnd\n ) {\n try {\n const currentIndex = firstGOPState.index as MP4Index;\n const videoTrack = currentIndex.tracks.video;\n\n if (videoTrack && videoTrack.gopIndex[0]) {\n // buffer accumulated from file start (position 0)\n // sample.byteOffset is absolute offset from file start\n // So byteStart should be 0 to match buffer's starting position\n const chunks = this.extractFirstGOP(firstGOPState.buffer, videoTrack, 0);\n\n if (chunks.length > 0) {\n options.onFirstFrameReady?.(currentIndex, chunks);\n }\n }\n\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to extract first GOP:', error);\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n }\n }\n }\n\n private setupAudioExtraction(\n mp4boxFile: MP4BoxFile,\n info: any\n ): {\n config?: AudioDecoderConfig;\n trackId?: number;\n timescale?: number;\n expectedSamples: number;\n isComplete: boolean;\n } {\n const audioTrack = info.tracks.find((t: any) => t.type === 'audio');\n if (audioTrack) {\n const trackId: number = audioTrack.id;\n const config = {\n codec: audioTrack.codec.startsWith('mp4a') ? 'mp4a.40.2' : audioTrack.codec,\n sampleRate: audioTrack.audio.sample_rate || audioTrack.timescale,\n numberOfChannels: audioTrack.audio.channel_count,\n };\n\n // Extract all audio samples\n mp4boxFile.setExtractionOptions(trackId, null as any, {\n nbSamples: Infinity, // Extract all samples\n });\n mp4boxFile.start();\n\n return {\n config,\n trackId,\n timescale: audioTrack.timescale, // Use track timescale for time calculations\n expectedSamples: audioTrack.nb_samples || Infinity,\n isComplete: false,\n };\n }\n\n return { expectedSamples: 0, isComplete: true };\n }\n\n /**\n * Parse from file/ArrayBuffer (for already cached resources)\n */\n async parseFromFile(data: File | ArrayBuffer): Promise<MP4Index> {\n const mp4boxFile = MP4Box.createFile();\n\n const indexPromise = new Promise<MP4Index>((resolve, reject) => {\n mp4boxFile.onError = (error: string) => {\n reject(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n const index = this.buildIndex(mp4boxFile, info);\n resolve(index);\n } catch (error) {\n reject(error);\n }\n };\n });\n\n // Read file data\n let buffer: ArrayBuffer;\n if (data instanceof File) {\n buffer = await data.arrayBuffer();\n } else {\n buffer = data;\n }\n\n (buffer as any).fileStart = 0;\n mp4boxFile.appendBuffer(buffer);\n mp4boxFile.flush();\n\n return indexPromise;\n }\n\n /**\n * Build MP4Index from mp4box.js parsed data\n */\n private buildIndex(mp4boxFile: MP4BoxFile, info: any): MP4Index {\n const index: MP4Index = {\n resourceId: '', // Will be set by caller\n moovOffset: 0, // mp4box doesn't expose this directly\n moovSize: 0,\n durationUs: (info.duration / info.timescale) * 1_000_000,\n tracks: {},\n };\n\n for (const trackInfo of info.tracks) {\n if (trackInfo.type === 'video') {\n index.tracks.video = this.buildVideoTrackIndex(mp4boxFile, trackInfo);\n } else if (trackInfo.type === 'audio') {\n index.tracks.audio = this.buildAudioTrackIndex(mp4boxFile, trackInfo);\n }\n }\n\n return index;\n }\n\n /**\n * Build video track index with sample table and GOP boundaries\n */\n private buildVideoTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): VideoTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n const gopIndex = this.buildGOPIndex(samples);\n const description = this.getVideoDescription(mp4boxFile, trackInfo);\n\n return {\n trackId: trackInfo.id,\n codec: normalizeVideoCodec(trackInfo.codec, description),\n width: trackInfo.track_width || trackInfo.video?.width || 0,\n height: trackInfo.track_height || trackInfo.video?.height || 0,\n timescale: trackInfo.timescale,\n description,\n samples,\n gopIndex,\n };\n }\n\n /**\n * Build audio track index\n */\n private buildAudioTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): AudioTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n sampleRate: trackInfo.audio?.sample_rate || 48000,\n numberOfChannels: trackInfo.audio?.channel_count || 2,\n timescale: trackInfo.timescale,\n samples,\n };\n }\n\n /**\n * Build sample table from mp4box track samples\n *\n * IMPORTANT: Keep samples in DTS (decode) order, not PTS (presentation) order!\n * VideoDecoder requires chunks in decode order. It will output frames in PTS order.\n */\n private buildSampleTable(mp4boxFile: MP4BoxFile, trackId: number, timescale: number): Sample[] {\n const samples: Sample[] = [];\n\n // Get track box\n const trak = mp4boxFile.getTrackById(trackId);\n if (!trak) return samples;\n\n // Access sample table (already in DTS order)\n const sampleTable = trak.samples || [];\n\n // Calculate PTS from CTS and normalize to start at 0\n let timestampOffset: number | null = null;\n\n for (const sample of sampleTable) {\n const durationUs = ((sample.duration || 0) / timescale) * 1_000_000;\n\n // Use CTS (Composition Time Stamp = PTS) for display timestamp\n const rawTimestampUs = ((sample.cts || 0) / timescale) * 1_000_000;\n\n // Normalize: first frame starts at 0 (like MP4Demuxer does)\n if (timestampOffset === null) {\n timestampOffset = rawTimestampUs;\n }\n const timestampUs = rawTimestampUs - timestampOffset;\n\n samples.push({\n timestamp: timestampUs, // Normalized PTS (display timestamp)\n duration: durationUs,\n byteOffset: sample.offset || 0,\n byteLength: sample.size || 0,\n isKeyframe: sample.is_sync || false,\n });\n }\n\n // DO NOT SORT! Samples must stay in DTS (decode) order for VideoDecoder\n // Decoder will output frames in PTS order automatically\n\n return samples;\n }\n\n /**\n * Extract video description (avcC/hvcC/etc) for VideoDecoder\n * Reuses MP4Demuxer.extractVideoDescription for consistency\n */\n private getVideoDescription(mp4boxFile: MP4BoxFile, trackInfo: any): ArrayBuffer | undefined {\n return MP4Demuxer.extractVideoDescription(mp4boxFile, trackInfo.id);\n }\n\n /**\n * Build GOP index from samples\n * GOP = Group of Pictures, starts with a keyframe\n */\n private buildGOPIndex(samples: Sample[]): GOP[] {\n const gopIndex: GOP[] = [];\n let currentGOP: {\n startTimeUs: TimeUs;\n keyframeSampleIndex: number;\n sampleCount: number;\n } | null = null;\n\n for (let i = 0; i < samples.length; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n if (sample.isKeyframe) {\n // Save previous GOP if exists\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n // Start new GOP\n currentGOP = {\n startTimeUs: sample.timestamp,\n keyframeSampleIndex: i,\n sampleCount: 1,\n };\n } else if (currentGOP) {\n // Add sample to current GOP\n currentGOP.sampleCount++;\n }\n }\n\n // Save last GOP\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n return gopIndex;\n }\n\n /**\n * Extract first GOP chunks from accumulated buffer\n * Used for fast cover rendering during streaming download\n */\n private extractFirstGOP(\n buffer: Uint8Array,\n videoTrack: VideoTrackIndex,\n byteStart: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const firstGOP = videoTrack.gopIndex[0];\n if (!firstGOP) {\n return chunks;\n }\n\n const samples = videoTrack.samples;\n\n for (let i = 0; i < firstGOP.sampleCount; i++) {\n const sampleIdx = firstGOP.keyframeSampleIndex + i;\n const sample = samples[sampleIdx];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - byteStart;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > buffer.length) {\n // Critical: If first sample (keyframe) is not fully downloaded, return empty array\n // to avoid decoding error \"A key frame is required after configure()\"\n if (i === 0) {\n console.warn(\n '[MP4IndexParser] First GOP keyframe not fully downloaded, skipping cover decode'\n );\n return []; // Return empty array to avoid sending non-keyframe as first chunk\n }\n\n console.warn('[MP4IndexParser] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n byteStart,\n relativeOffset,\n bufferLength: buffer.length,\n isKeyframe: sample.isKeyframe,\n sampleIndex: i,\n });\n\n // If not first sample, safe to skip (keyframe already present)\n continue;\n }\n\n const sampleData = buffer.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n try {\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 } catch (error) {\n console.warn('[MP4IndexParser] Failed to create EncodedVideoChunk:', error);\n }\n }\n\n // Additional safety check: ensure first chunk is keyframe\n if (chunks.length > 0 && chunks[0]?.type !== 'key') {\n console.error('[MP4IndexParser] First chunk is not a keyframe, discarding all chunks');\n return [];\n }\n\n return chunks;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MP4IndexParser.js","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"sourcesContent":["import { MP4Box } from '../../utils/mp4box';\nimport type { MP4BoxFile } from 'mp4box';\nimport type { MP4Index, VideoTrackIndex, AudioTrackIndex, Sample, GOP } from './types';\nimport type { TimeUs } from '../../model/types';\nimport { MP4Demuxer, normalizeVideoCodec } from './MP4Demuxer';\nimport { EmptyStreamError } from '../../utils/errors';\n\nexport interface MP4ParseResult {\n index: MP4Index;\n audioSamples?: EncodedAudioChunk[];\n audioMetadata?: AudioDecoderConfig;\n}\n\nexport interface ParseStreamOptions {\n resourceId?: string;\n onFirstFrameReady?: (index: MP4Index, firstGOPChunks: EncodedVideoChunk[]) => void;\n}\n\n/**\n * MP4IndexParser - Parse MP4 moov box and build time→byte index\n *\n * Features:\n * - Stream-based moov parsing (stop after moov found)\n * - Build sample table with byte offsets\n * - Build GOP index for keyframe positions\n * - Extract all audio samples (encoded) for memory caching\n * - Extract first GOP for fast cover rendering\n */\nexport class MP4IndexParser {\n /**\n * Parse from streaming download\n * Returns video index + all audio samples\n * Can optionally extract first GOP for fast cover rendering\n */\n async parseFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: ParseStreamOptions\n ): Promise<MP4ParseResult> {\n const resourceId = options?.resourceId || 'unknown';\n const mp4boxFile = MP4Box.createFile();\n let resolveResult: ((result: MP4ParseResult) => void) | null = null;\n let rejectResult: ((error: Error) => void) | null = null;\n\n const audioChunks: EncodedAudioChunk[] = [];\n let audioConfig: AudioDecoderConfig | undefined;\n let audioTrackId: number | undefined;\n let audioTimescale: number | undefined;\n let expectedAudioSamples = 0;\n let savedInfo: any = null;\n\n const resultPromise = new Promise<MP4ParseResult>((resolve, reject) => {\n resolveResult = resolve;\n rejectResult = reject;\n });\n\n // First GOP extraction state\n const firstGOPState = {\n byteEnd: 0,\n extracted: !options?.onFirstFrameReady,\n index: null as MP4Index | null,\n buffer: new Uint8Array(0),\n };\n\n const streamState = {\n fileOffset: 0,\n moovParsed: false,\n audioComplete: false,\n };\n\n mp4boxFile.onError = (error: string) => {\n rejectResult?.(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n // Set moovParsed immediately\n streamState.moovParsed = true;\n\n // Save info for later use in audio extraction\n savedInfo = info;\n\n // Build video index\n const index = this.buildIndex(mp4boxFile, info);\n firstGOPState.index = index;\n\n // Check if this is a fast-start format (moov at beginning)\n const isFastStart = info.isProgressive === true;\n\n // Only extract first GOP for fast-start videos (moov-at-end videos will read from OPFS)\n if (isFastStart && options?.onFirstFrameReady && index.tracks.video?.gopIndex[0]) {\n const firstGOP = index.tracks.video.gopIndex[0];\n const samples = index.tracks.video.samples;\n const startIdx = firstGOP.keyframeSampleIndex;\n const endIdx = startIdx + firstGOP.sampleCount;\n\n const endSample = samples[endIdx - 1];\n if (endSample) {\n firstGOPState.byteEnd = endSample.byteOffset + endSample.byteLength;\n }\n }\n\n // Setup audio extraction if audio track exists.\n // IMPORTANT: mp4boxFile.start() may synchronously trigger onSamples.\n // We must assign audioTrackId/audioConfig first, otherwise the onSamples handler\n // (which guards by audioTrackId && audioConfig) may drop all samples.\n const { config, trackId, timescale, expectedSamples, isComplete } =\n this.setupAudioExtraction(mp4boxFile, info);\n audioConfig = config;\n audioTrackId = trackId;\n audioTimescale = timescale;\n expectedAudioSamples = expectedSamples;\n streamState.audioComplete = isComplete;\n\n if (!isComplete && audioTrackId && audioConfig) {\n mp4boxFile.start();\n }\n\n if (isComplete) {\n // No audio track, resolve immediately if firstGOP extracted or not needed\n // Use setTimeout to allow stack to unwind and ensure consistency\n setTimeout(() => {\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n resolveResult?.({ index });\n }\n }, 0);\n }\n } catch (error) {\n rejectResult?.(error as Error);\n }\n };\n\n // Handle audio sample extraction\n mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n if (trackId === audioTrackId && audioConfig) {\n // Use track timescale, fallback to sampleRate if timescale not available\n // MP4 sample.cts and sample.duration are in track timescale units\n const timescale = audioTimescale || audioConfig.sampleRate;\n\n // Process samples if any\n if (samples && samples.length > 0) {\n for (const sample of samples) {\n try {\n const chunk = new EncodedAudioChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp: Math.round((sample.cts / timescale) * 1_000_000),\n duration: Math.round((sample.duration / timescale) * 1_000_000),\n data: sample.data,\n });\n audioChunks.push(chunk);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create audio chunk:', error);\n }\n }\n }\n\n // Check if we have all samples\n if (expectedAudioSamples !== Infinity && audioChunks.length >= expectedAudioSamples) {\n streamState.audioComplete = true;\n }\n }\n };\n\n await this.processStreamData(\n stream,\n mp4boxFile,\n options,\n firstGOPState,\n streamState,\n resourceId\n );\n\n // Flush remaining data\n mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onReady/onSamples are async)\n // Reference: MP4Demuxer.ts line 302\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // If audio extraction finished normally (or there was no audio), resolve now\n if (streamState.moovParsed && streamState.audioComplete) {\n const index = firstGOPState.index || this.buildIndex(mp4boxFile, savedInfo);\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n }\n // If moov is parsed but audio extraction hasn't completed yet, force complete\n else if (streamState.moovParsed && !streamState.audioComplete && savedInfo) {\n streamState.audioComplete = true;\n const index = this.buildIndex(mp4boxFile, savedInfo);\n\n // Check if we have audio chunks\n if (audioChunks.length > 0 && audioConfig) {\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n } else {\n // No audio or extraction failed, just return index\n resolveResult!({ index });\n }\n }\n\n // Wait for result with timeout (5s max)\n const parseTimeout = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `MP4Box parsing timeout after reading ${streamState.fileOffset} bytes. ` +\n `moovParsed=${streamState.moovParsed}, audioExtractionComplete=${streamState.audioComplete}`\n )\n );\n }, 5000);\n });\n\n // Wait for either resultPromise or timeout\n return await Promise.race([resultPromise, parseTimeout]);\n }\n\n private async processStreamData(\n stream: ReadableStream<Uint8Array>,\n mp4boxFile: MP4BoxFile,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any,\n resourceId: string\n ): Promise<void> {\n const reader = stream.getReader();\n\n try {\n let hasData = false;\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n hasData = true;\n const buffer = this.prepareBuffer(value, streamState.fileOffset);\n\n this.handleFirstGOPExtraction(buffer, value, options, firstGOPState, streamState);\n\n mp4boxFile.appendBuffer(buffer);\n streamState.fileOffset += buffer.byteLength;\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n break;\n }\n }\n }\n\n if (!hasData) {\n throw new EmptyStreamError(resourceId, streamState.fileOffset);\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private prepareBuffer(value: Uint8Array, fileOffset: number): ArrayBuffer {\n let buffer: ArrayBuffer;\n if (value.byteOffset === 0 && value.byteLength === value.buffer.byteLength) {\n buffer = value.buffer as ArrayBuffer;\n } else {\n buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n ) as ArrayBuffer;\n }\n (buffer as any).fileStart = fileOffset;\n return buffer;\n }\n\n private handleFirstGOPExtraction(\n buffer: ArrayBuffer,\n value: Uint8Array,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): void {\n if (!firstGOPState.extracted && options?.onFirstFrameReady) {\n // Safety: Cap accumulation to 10MB\n if (firstGOPState.buffer.length + value.length < 10 * 1024 * 1024) {\n const newBuffer = new Uint8Array(firstGOPState.buffer.length + value.length);\n newBuffer.set(firstGOPState.buffer);\n newBuffer.set(value, firstGOPState.buffer.length);\n firstGOPState.buffer = newBuffer;\n } else if (!streamState.moovParsed) {\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n\n if (\n streamState.moovParsed &&\n firstGOPState.byteEnd > 0 &&\n firstGOPState.index &&\n streamState.fileOffset + buffer.byteLength >= firstGOPState.byteEnd\n ) {\n try {\n const currentIndex = firstGOPState.index as MP4Index;\n const videoTrack = currentIndex.tracks.video;\n\n if (videoTrack && videoTrack.gopIndex[0]) {\n // buffer accumulated from file start (position 0)\n // sample.byteOffset is absolute offset from file start\n // So byteStart should be 0 to match buffer's starting position\n const chunks = this.extractFirstGOP(firstGOPState.buffer, videoTrack, 0);\n\n if (chunks.length > 0) {\n options.onFirstFrameReady?.(currentIndex, chunks);\n }\n }\n\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to extract first GOP:', error);\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n }\n }\n }\n\n private setupAudioExtraction(\n mp4boxFile: MP4BoxFile,\n info: any\n ): {\n config?: AudioDecoderConfig;\n trackId?: number;\n timescale?: number;\n expectedSamples: number;\n isComplete: boolean;\n } {\n const audioTrack = info.tracks.find((t: any) => t.type === 'audio');\n if (audioTrack) {\n const trackId: number = audioTrack.id;\n const config = {\n codec: audioTrack.codec.startsWith('mp4a') ? 'mp4a.40.2' : audioTrack.codec,\n sampleRate: audioTrack.audio.sample_rate || audioTrack.timescale,\n numberOfChannels: audioTrack.audio.channel_count,\n };\n\n // Extract all audio samples.\n // mp4box.js expects the track info object as the \"user\" parameter for reliable extraction.\n // Passing null can result in missing onSamples callbacks for some files.\n mp4boxFile.setExtractionOptions(trackId, audioTrack, {\n nbSamples: Infinity, // Extract all samples\n });\n\n return {\n config,\n trackId,\n timescale: audioTrack.timescale, // Use track timescale for time calculations\n expectedSamples: audioTrack.nb_samples || Infinity,\n isComplete: false,\n };\n }\n\n return { expectedSamples: 0, isComplete: true };\n }\n\n /**\n * Parse from file/ArrayBuffer (for already cached resources)\n */\n async parseFromFile(data: File | ArrayBuffer): Promise<MP4Index> {\n const mp4boxFile = MP4Box.createFile();\n\n const indexPromise = new Promise<MP4Index>((resolve, reject) => {\n mp4boxFile.onError = (error: string) => {\n reject(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n const index = this.buildIndex(mp4boxFile, info);\n resolve(index);\n } catch (error) {\n reject(error);\n }\n };\n });\n\n // Read file data\n let buffer: ArrayBuffer;\n if (data instanceof File) {\n buffer = await data.arrayBuffer();\n } else {\n buffer = data;\n }\n\n (buffer as any).fileStart = 0;\n mp4boxFile.appendBuffer(buffer);\n mp4boxFile.flush();\n\n return indexPromise;\n }\n\n /**\n * Build MP4Index from mp4box.js parsed data\n */\n private buildIndex(mp4boxFile: MP4BoxFile, info: any): MP4Index {\n const index: MP4Index = {\n resourceId: '', // Will be set by caller\n moovOffset: 0, // mp4box doesn't expose this directly\n moovSize: 0,\n durationUs: (info.duration / info.timescale) * 1_000_000,\n tracks: {},\n };\n\n for (const trackInfo of info.tracks) {\n if (trackInfo.type === 'video') {\n index.tracks.video = this.buildVideoTrackIndex(mp4boxFile, trackInfo);\n } else if (trackInfo.type === 'audio') {\n index.tracks.audio = this.buildAudioTrackIndex(mp4boxFile, trackInfo);\n }\n }\n\n return index;\n }\n\n /**\n * Build video track index with sample table and GOP boundaries\n */\n private buildVideoTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): VideoTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n const gopIndex = this.buildGOPIndex(samples);\n const description = this.getVideoDescription(mp4boxFile, trackInfo);\n\n return {\n trackId: trackInfo.id,\n codec: normalizeVideoCodec(trackInfo.codec, description),\n width: trackInfo.track_width || trackInfo.video?.width || 0,\n height: trackInfo.track_height || trackInfo.video?.height || 0,\n timescale: trackInfo.timescale,\n description,\n samples,\n gopIndex,\n };\n }\n\n /**\n * Build audio track index\n */\n private buildAudioTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): AudioTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n sampleRate: trackInfo.audio?.sample_rate || 48000,\n numberOfChannels: trackInfo.audio?.channel_count || 2,\n timescale: trackInfo.timescale,\n samples,\n };\n }\n\n /**\n * Build sample table from mp4box track samples\n *\n * IMPORTANT: Keep samples in DTS (decode) order, not PTS (presentation) order!\n * VideoDecoder requires chunks in decode order. It will output frames in PTS order.\n */\n private buildSampleTable(mp4boxFile: MP4BoxFile, trackId: number, timescale: number): Sample[] {\n const samples: Sample[] = [];\n\n // Get track box\n const trak = mp4boxFile.getTrackById(trackId);\n if (!trak) return samples;\n\n // Access sample table (already in DTS order)\n const sampleTable = trak.samples || [];\n\n // Calculate PTS from CTS and normalize to start at 0\n let timestampOffset: number | null = null;\n\n for (const sample of sampleTable) {\n const durationUs = ((sample.duration || 0) / timescale) * 1_000_000;\n\n // Use CTS (Composition Time Stamp = PTS) for display timestamp\n const rawTimestampUs = ((sample.cts || 0) / timescale) * 1_000_000;\n\n // Normalize: first frame starts at 0 (like MP4Demuxer does)\n if (timestampOffset === null) {\n timestampOffset = rawTimestampUs;\n }\n const timestampUs = rawTimestampUs - timestampOffset;\n\n samples.push({\n timestamp: timestampUs, // Normalized PTS (display timestamp)\n duration: durationUs,\n byteOffset: sample.offset || 0,\n byteLength: sample.size || 0,\n isKeyframe: sample.is_sync || false,\n });\n }\n\n // DO NOT SORT! Samples must stay in DTS (decode) order for VideoDecoder\n // Decoder will output frames in PTS order automatically\n\n return samples;\n }\n\n /**\n * Extract video description (avcC/hvcC/etc) for VideoDecoder\n * Reuses MP4Demuxer.extractVideoDescription for consistency\n */\n private getVideoDescription(mp4boxFile: MP4BoxFile, trackInfo: any): ArrayBuffer | undefined {\n return MP4Demuxer.extractVideoDescription(mp4boxFile, trackInfo.id);\n }\n\n /**\n * Build GOP index from samples\n * GOP = Group of Pictures, starts with a keyframe\n */\n private buildGOPIndex(samples: Sample[]): GOP[] {\n const gopIndex: GOP[] = [];\n let currentGOP: {\n startTimeUs: TimeUs;\n keyframeSampleIndex: number;\n sampleCount: number;\n } | null = null;\n\n for (let i = 0; i < samples.length; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n if (sample.isKeyframe) {\n // Save previous GOP if exists\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n // Start new GOP\n currentGOP = {\n startTimeUs: sample.timestamp,\n keyframeSampleIndex: i,\n sampleCount: 1,\n };\n } else if (currentGOP) {\n // Add sample to current GOP\n currentGOP.sampleCount++;\n }\n }\n\n // Save last GOP\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n return gopIndex;\n }\n\n /**\n * Extract first GOP chunks from accumulated buffer\n * Used for fast cover rendering during streaming download\n */\n private extractFirstGOP(\n buffer: Uint8Array,\n videoTrack: VideoTrackIndex,\n byteStart: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const firstGOP = videoTrack.gopIndex[0];\n if (!firstGOP) {\n return chunks;\n }\n\n const samples = videoTrack.samples;\n\n for (let i = 0; i < firstGOP.sampleCount; i++) {\n const sampleIdx = firstGOP.keyframeSampleIndex + i;\n const sample = samples[sampleIdx];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - byteStart;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > buffer.length) {\n // Critical: If first sample (keyframe) is not fully downloaded, return empty array\n // to avoid decoding error \"A key frame is required after configure()\"\n if (i === 0) {\n console.warn(\n '[MP4IndexParser] First GOP keyframe not fully downloaded, skipping cover decode'\n );\n return []; // Return empty array to avoid sending non-keyframe as first chunk\n }\n\n console.warn('[MP4IndexParser] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n byteStart,\n relativeOffset,\n bufferLength: buffer.length,\n isKeyframe: sample.isKeyframe,\n sampleIndex: i,\n });\n\n // If not first sample, safe to skip (keyframe already present)\n continue;\n }\n\n const sampleData = buffer.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n try {\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 } catch (error) {\n console.warn('[MP4IndexParser] Failed to create EncodedVideoChunk:', error);\n }\n }\n\n // Additional safety check: ensure first chunk is keyframe\n if (chunks.length > 0 && chunks[0]?.type !== 'key') {\n console.error('[MP4IndexParser] First chunk is not a keyframe, discarding all chunks');\n return [];\n }\n\n return chunks;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { StreamTarget, ArrayBufferTarget, Muxer } from "../../node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
1
+ import { StreamTarget, ArrayBufferTarget, Muxer } from "../../medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
2
2
  class MP4Muxer {
3
3
  muxer;
4
4
  firstVideoChunk = true;
@@ -0,0 +1,16 @@
1
+ import { TimeUs } from '../model/types';
2
+
3
+ export interface LoopSegment {
4
+ clipRelativeStartUs: TimeUs;
5
+ durationUs: TimeUs;
6
+ resourceStartUs: TimeUs;
7
+ resourceEndUs: TimeUs;
8
+ }
9
+ export declare function buildLoopedResourceSegments(params: {
10
+ clipRelativeStartUs: TimeUs;
11
+ clipRelativeEndUs: TimeUs;
12
+ trimStartUs: TimeUs;
13
+ resourceDurationUs: TimeUs;
14
+ loop: boolean;
15
+ }): LoopSegment[];
16
+ //# sourceMappingURL=loop-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-utils.d.ts","sourceRoot":"","sources":["../../src/utils/loop-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,WAAW,EAAE,CA+ChB"}