@meframe/core 0.0.28 → 0.0.30-beta

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 (220) hide show
  1. package/dist/Meframe.d.ts +2 -13
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +6 -100
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +35 -19
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +223 -134
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/VideoL1Cache.d.ts +15 -2
  10. package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/VideoL1Cache.js +58 -38
  12. package/dist/cache/l1/VideoL1Cache.js.map +1 -1
  13. package/dist/cache/l2/L2Cache.d.ts.map +1 -1
  14. package/dist/cache/l2/L2Cache.js +5 -5
  15. package/dist/cache/l2/L2Cache.js.map +1 -1
  16. package/dist/cache/l2/L2OPFSStore.d.ts +37 -0
  17. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  18. package/dist/cache/l2/L2OPFSStore.js +89 -0
  19. package/dist/cache/l2/L2OPFSStore.js.map +1 -0
  20. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  21. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  22. package/dist/cache/resource/AudioSampleCache.js +69 -0
  23. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  24. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  25. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  26. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  27. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  28. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  29. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  30. package/dist/cache/resource/MP4IndexCache.js +104 -0
  31. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  32. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  33. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  34. package/dist/cache/resource/ResourceCache.js +92 -0
  35. package/dist/cache/resource/ResourceCache.js.map +1 -0
  36. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  37. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  38. package/dist/cache/{l2/IndexedDBStore.js → storage/indexeddb/ChunkRecordStore.js} +3 -3
  39. package/dist/cache/storage/indexeddb/ChunkRecordStore.js.map +1 -0
  40. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  41. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  42. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  43. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  44. package/dist/cache/storage/opfs/types.d.ts +16 -0
  45. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +21 -2
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/types.d.ts +28 -0
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/controllers/ExportController.d.ts +16 -0
  52. package/dist/controllers/ExportController.d.ts.map +1 -0
  53. package/dist/controllers/ExportController.js +44 -0
  54. package/dist/controllers/ExportController.js.map +1 -0
  55. package/dist/controllers/PlaybackController.d.ts +28 -4
  56. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  57. package/dist/controllers/PlaybackController.js +117 -52
  58. package/dist/controllers/PlaybackController.js.map +1 -1
  59. package/dist/controllers/index.d.ts +2 -3
  60. package/dist/controllers/index.d.ts.map +1 -1
  61. package/dist/controllers/types.d.ts +0 -28
  62. package/dist/controllers/types.d.ts.map +1 -1
  63. package/dist/event/events.d.ts +8 -0
  64. package/dist/event/events.d.ts.map +1 -1
  65. package/dist/event/events.js +1 -0
  66. package/dist/event/events.js.map +1 -1
  67. package/dist/model/CompositionModel.d.ts.map +1 -1
  68. package/dist/model/CompositionModel.js +11 -6
  69. package/dist/model/CompositionModel.js.map +1 -1
  70. package/dist/model/RcFrame.d.ts +2 -0
  71. package/dist/model/RcFrame.d.ts.map +1 -1
  72. package/dist/model/RcFrame.js +3 -0
  73. package/dist/model/RcFrame.js.map +1 -1
  74. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  75. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  76. package/dist/orchestrator/ExportScheduler.js +241 -0
  77. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  78. package/dist/orchestrator/GlobalAudioSession.d.ts +21 -7
  79. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  80. package/dist/orchestrator/GlobalAudioSession.js +132 -140
  81. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  82. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  83. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  84. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  85. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  86. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  87. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  88. package/dist/orchestrator/Orchestrator.js +234 -301
  89. package/dist/orchestrator/Orchestrator.js.map +1 -1
  90. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  91. package/dist/orchestrator/VideoClipSession.js +3 -15
  92. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  93. package/dist/orchestrator/index.d.ts +0 -1
  94. package/dist/orchestrator/index.d.ts.map +1 -1
  95. package/dist/orchestrator/types.d.ts +4 -4
  96. package/dist/orchestrator/types.d.ts.map +1 -1
  97. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  98. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  99. package/dist/stages/compose/FilterProcessor.js +226 -0
  100. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  101. package/dist/stages/compose/FrameRateConverter.d.ts +68 -0
  102. package/dist/stages/compose/FrameRateConverter.d.ts.map +1 -0
  103. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  104. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  105. package/dist/stages/compose/LayerRenderer.js +270 -0
  106. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  107. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  108. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  109. package/dist/stages/compose/TransitionProcessor.js +189 -0
  110. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  111. package/dist/stages/compose/VideoComposer.d.ts +6 -4
  112. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  113. package/dist/stages/compose/VideoComposer.js +229 -0
  114. package/dist/stages/compose/VideoComposer.js.map +1 -0
  115. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  116. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  117. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  118. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  119. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  120. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  121. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  122. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  123. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  124. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  125. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  126. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  127. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  128. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  129. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  130. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  131. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  132. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  133. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  134. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  135. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  136. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  137. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  138. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  139. package/dist/stages/compose/types.d.ts +2 -1
  140. package/dist/stages/compose/types.d.ts.map +1 -1
  141. package/dist/stages/decode/BaseDecoder.js +0 -3
  142. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  143. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  144. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  145. package/dist/stages/demux/MP4Demuxer.js +281 -0
  146. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  147. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  148. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  149. package/dist/stages/demux/MP4IndexParser.js +416 -0
  150. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  151. package/dist/stages/demux/types.d.ts +48 -0
  152. package/dist/stages/demux/types.d.ts.map +1 -1
  153. package/dist/stages/encode/index.d.ts +0 -1
  154. package/dist/stages/encode/index.d.ts.map +1 -1
  155. package/dist/stages/load/ResourceLoader.d.ts +44 -2
  156. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  157. package/dist/stages/load/ResourceLoader.js +281 -37
  158. package/dist/stages/load/ResourceLoader.js.map +1 -1
  159. package/dist/stages/load/TaskManager.d.ts +6 -2
  160. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  161. package/dist/stages/load/TaskManager.js +27 -4
  162. package/dist/stages/load/TaskManager.js.map +1 -1
  163. package/dist/stages/load/types.d.ts +7 -0
  164. package/dist/stages/load/types.d.ts.map +1 -1
  165. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  166. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  167. package/dist/stages/mux/MP4Muxer.js +24 -13
  168. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  169. package/dist/stages/mux/MuxManager.d.ts +10 -21
  170. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  171. package/dist/stages/mux/MuxManager.js +21 -162
  172. package/dist/stages/mux/MuxManager.js.map +1 -1
  173. package/dist/stages/mux/index.d.ts +0 -1
  174. package/dist/stages/mux/index.d.ts.map +1 -1
  175. package/dist/utils/binary-search.d.ts +12 -4
  176. package/dist/utils/binary-search.d.ts.map +1 -1
  177. package/dist/utils/binary-search.js +52 -6
  178. package/dist/utils/binary-search.js.map +1 -1
  179. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  180. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  181. package/dist/workers/{MP4Demuxer.CFHDkPYc.js → MP4Demuxer.BEa6PLJm.js} +10 -3
  182. package/dist/workers/{MP4Demuxer.CFHDkPYc.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  183. package/dist/workers/stages/compose/{video-compose.worker.M5uomNVr.js → video-compose.worker.DHQ8B105.js} +260 -83
  184. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  185. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  186. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  187. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  188. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  189. package/dist/workers/stages/demux/{audio-demux.worker.BTFPcY7P.js → audio-demux.worker._VRQdLdv.js} +2 -2
  190. package/dist/workers/stages/demux/{audio-demux.worker.BTFPcY7P.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  191. package/dist/workers/stages/demux/{video-demux.worker.D_WeHPkt.js → video-demux.worker.CSkxGtmx.js} +3 -19
  192. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  193. package/dist/workers/worker-manifest.json +5 -5
  194. package/package.json +1 -1
  195. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  196. package/dist/cache/l2/OPFSStore.js +0 -131
  197. package/dist/cache/l2/OPFSStore.js.map +0 -1
  198. package/dist/controllers/PreRenderService.d.ts +0 -59
  199. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  200. package/dist/controllers/PreRenderService.js +0 -185
  201. package/dist/controllers/PreRenderService.js.map +0 -1
  202. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  203. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  204. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  205. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  206. package/dist/orchestrator/ClipSessionManager.js +0 -158
  207. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  208. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  209. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  210. package/dist/stages/encode/ClipEncoderManager.d.ts +0 -64
  211. package/dist/stages/encode/ClipEncoderManager.d.ts.map +0 -1
  212. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  213. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  214. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  215. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  216. package/dist/utils/time-utils.js +0 -45
  217. package/dist/utils/time-utils.js.map +0 -1
  218. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  219. package/dist/workers/stages/compose/video-compose.worker.M5uomNVr.js.map +0 -1
  220. package/dist/workers/stages/demux/video-demux.worker.D_WeHPkt.js.map +0 -1
@@ -1,18 +1,16 @@
1
1
  import { EventBus } from "../event/EventBus.js";
2
2
  import { WorkerPool } from "../worker/WorkerPool.js";
3
3
  import { applyPatch } from "../model/patch.js";
4
- import { ResourceLoader, ResourceConflictError } from "../stages/load/ResourceLoader.js";
4
+ import { ResourceLoader } from "../stages/load/ResourceLoader.js";
5
5
  import { CacheManager } from "../cache/CacheManager.js";
6
6
  import { ConfigLoader } from "../config/ConfigLoader.js";
7
- import { hasResourceId } from "../model/types.js";
7
+ import { isVideoClip, hasResourceId } from "../model/types.js";
8
8
  import { MeframeEvent } from "../event/events.js";
9
9
  import { CompositionPlanner } from "./CompositionPlanner.js";
10
- import { VideoClipSession } from "./VideoClipSession.js";
11
- import { ClipSessionManager } from "./ClipSessionManager.js";
12
10
  import { GlobalAudioSession } from "./GlobalAudioSession.js";
13
11
  import { MuxManager } from "../stages/mux/MuxManager.js";
14
- import { VideoChunkDecoder } from "../stages/decode/VideoChunkDecoder.js";
15
- import { quantizeTimestampToFrame } from "../utils/time-utils.js";
12
+ import { OnDemandVideoSession } from "./OnDemandVideoSession.js";
13
+ import { ExportScheduler } from "./ExportScheduler.js";
16
14
  class Orchestrator {
17
15
  workers;
18
16
  eventBus;
@@ -22,13 +20,11 @@ class Orchestrator {
22
20
  planner;
23
21
  audioSession;
24
22
  muxManager;
25
- activeClips = /* @__PURE__ */ new Set();
23
+ exportScheduler;
26
24
  isInitialized = false;
27
25
  config = ConfigLoader.getInstance().getConfig();
28
- clipSessionManager;
29
- currentClipId = null;
30
26
  ensureCacheDebounceTimer = null;
31
- ensureCacheDebounceDelay = 150;
27
+ currentClipId = null;
32
28
  events;
33
29
  constructor(config) {
34
30
  this.eventBus = config.eventBus || new EventBus();
@@ -64,13 +60,6 @@ class Orchestrator {
64
60
  },
65
61
  this.eventBus
66
62
  );
67
- this.clipSessionManager = new ClipSessionManager({
68
- maxConcurrent: 2,
69
- factory: {
70
- createSession: (clipId) => this.createSession(clipId)
71
- },
72
- cacheManager: this.cacheManager
73
- });
74
63
  this.audioSession = new GlobalAudioSession({
75
64
  cacheManager: this.cacheManager,
76
65
  workers: this.workers,
@@ -84,26 +73,48 @@ class Orchestrator {
84
73
  this.audioSession,
85
74
  this.config.encode.audio
86
75
  );
76
+ this.exportScheduler = new ExportScheduler({
77
+ workerPool: this.workers,
78
+ planner: this.planner,
79
+ cacheManager: this.cacheManager,
80
+ resourceLoader: this.resourceLoader,
81
+ muxManager: this.muxManager,
82
+ audioSession: this.audioSession,
83
+ workerConfigsProvider: () => this.buildWorkerConfigs(),
84
+ eventBus: this.eventBus
85
+ });
86
+ this.setupResourceFirstFrameHandler();
87
+ this.setupPreloadHandlers();
87
88
  }
88
- get workerStatus() {
89
- const status = this.workers.status;
90
- const result = {};
91
- const workerTypes = [
92
- "videoDemux",
93
- "audioDemux",
94
- "videoDecode",
95
- "audioDecode",
96
- "videoCompose",
97
- "audioCompose",
98
- "videoEncode"
99
- ];
100
- for (const type of workerTypes) {
101
- result[type] = status[type] || {
102
- state: "idle",
103
- taskCount: 0
104
- };
105
- }
106
- return result;
89
+ setupResourceFirstFrameHandler() {
90
+ this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {
91
+ const { resourceId, clipId, index, chunks } = payload;
92
+ if (!this.compositionModel) return;
93
+ const clip = this.compositionModel.findClip(clipId);
94
+ if (!clip || !clip.trackId) return;
95
+ if (clip.startUs === 0) {
96
+ const fps = this.compositionModel.fps ?? 30;
97
+ await OnDemandVideoSession.decodeAndCacheFirstFrame(
98
+ resourceId,
99
+ chunks,
100
+ index,
101
+ clip,
102
+ this.cacheManager,
103
+ fps
104
+ );
105
+ }
106
+ });
107
+ }
108
+ setupPreloadHandlers() {
109
+ this.eventBus.on(MeframeEvent.PlaybackPlay, () => {
110
+ this.resourceLoader.setPreloadingEnabled(false);
111
+ });
112
+ this.eventBus.on(MeframeEvent.PlaybackPause, () => {
113
+ this.resourceLoader.setPreloadingEnabled(true);
114
+ });
115
+ this.eventBus.on(MeframeEvent.PlaybackStop, () => {
116
+ this.resourceLoader.setPreloadingEnabled(true);
117
+ });
107
118
  }
108
119
  async initialize() {
109
120
  if (this.isInitialized) return;
@@ -130,31 +141,25 @@ class Orchestrator {
130
141
  }
131
142
  this.compositionModel = model;
132
143
  this.planner.setModel(model);
133
- this.currentClipId = null;
134
144
  this.eventBus.emit(MeframeEvent.ModelSet, model);
135
145
  this.eventBus.emit(MeframeEvent.CompositionUpdated, {
136
146
  trackCount: model.tracks.length,
137
147
  clipCount: model.tracks.reduce((acc, track) => acc + track.clips.length, 0),
138
148
  durationUs: model.durationUs
139
149
  });
140
- await this.audioSession.activateAllAudioClips();
141
150
  }
142
151
  async applyPatch(patch) {
143
152
  if (!this.compositionModel) {
144
153
  throw new Error("No composition model set");
145
154
  }
146
155
  const affectedClipIds = applyPatch(this.compositionModel, patch);
147
- const clipUpdates = this.planner.applyPatch(patch, affectedClipIds);
156
+ this.planner.applyPatch(patch, affectedClipIds);
148
157
  this.eventBus.emit(MeframeEvent.PatchApplied, {
149
158
  operations: patch.operations.length,
150
159
  affectedClips: Array.from(affectedClipIds)
151
160
  });
152
- for (const update of clipUpdates) {
153
- if (update.type === "remove") {
154
- this.activeClips.delete(update.clipId);
155
- }
156
- this.cacheManager.invalidateClip(update.clipId);
157
- await this.clipSessionManager.handlePlannerUpdate(update.clipId, update);
161
+ for (const clipId of affectedClipIds) {
162
+ this.cacheManager.invalidateClip(clipId);
158
163
  }
159
164
  const reactivatedAudioClips = [];
160
165
  const reactivatedVideoClips = [];
@@ -185,62 +190,10 @@ class Orchestrator {
185
190
  if (state !== "ready") {
186
191
  return;
187
192
  }
188
- const resource = this.compositionModel.getResource(resourceId);
189
- if (!resource) {
190
- return;
191
- }
192
- if (resource.type === "video" || resource.type === "audio") {
193
- return;
194
- }
195
- const clipIds = this.compositionModel.getClipIdsByResourceId(resourceId);
196
- for (const clipId of clipIds) {
197
- if (!this.clipSessionManager.isClipActive(clipId)) {
198
- continue;
199
- }
200
- const clip = this.compositionModel.findClip(clipId);
201
- if (!clip) {
202
- continue;
203
- }
204
- const instructions = this.planner.getInstructions(clipId);
205
- if (!instructions) {
206
- continue;
207
- }
208
- const session = this.clipSessionManager.getSession(clipId);
209
- const composeWorker = session?.composeWorker;
210
- if (composeWorker) {
211
- composeWorker.send("install_instructions", instructions);
212
- }
213
- }
214
- }
215
- async restartWorker(type, clipId) {
216
- const clipLocalTypes = [
217
- "videoDemux",
218
- "audioDemux",
219
- "videoDecode",
220
- "audioDecode",
221
- "videoCompose",
222
- "videoEncode"
223
- ];
224
- if (clipLocalTypes.includes(type) && !clipId) {
225
- throw new Error(`clipId required for restarting ${type} worker`);
226
- }
227
- this.workers.terminate(type, clipId);
228
- const worker = await this.workers.getOrCreate(type, clipId);
229
- this.eventBus.emit(MeframeEvent.WorkerRestarted, {
230
- type,
231
- workerId: worker.getWorkerId(),
232
- reason: "Manual restart"
233
- });
234
- if (clipId) {
235
- const session = this.clipSessionManager.getSession(clipId);
236
- if (session) {
237
- await session.activate();
238
- }
239
- }
240
193
  }
241
- async renderFrame(timeUs, options) {
194
+ async getFrame(timeUs, options) {
242
195
  const signal = options?.signal;
243
- const immediate = options?.immediate ?? true;
196
+ const preheat = options?.preheat ?? false;
244
197
  if (!this.compositionModel) {
245
198
  throw new Error("No composition model set");
246
199
  }
@@ -248,113 +201,89 @@ class Orchestrator {
248
201
  if (!clip) {
249
202
  return null;
250
203
  }
251
- if (this.currentClipId !== clip.id) {
252
- this.currentClipId = clip.id;
253
- void this.ensureClipCache(timeUs, immediate);
254
- }
255
204
  let relativeTimeUs = options?.relativeTimeUs ?? timeUs - clip.startUs;
256
- relativeTimeUs = quantizeTimestampToFrame(relativeTimeUs, 0, this.compositionModel.fps);
257
205
  relativeTimeUs = Math.min(relativeTimeUs, clip.durationUs - 1);
258
- const cachedFrame = this.cacheManager.getFrame(relativeTimeUs, clip.id);
259
- if (cachedFrame) {
260
- this.eventBus.emit(MeframeEvent.CacheHit, {
206
+ if (!preheat) {
207
+ this.cacheManager.setWindow(timeUs);
208
+ const cachedFrame = this.cacheManager.getFrame(relativeTimeUs, clip.id);
209
+ if (cachedFrame) {
210
+ this.preheatNextClip(clip);
211
+ this.eventBus.emit(MeframeEvent.CacheHit, {
212
+ timeUs,
213
+ level: "L1",
214
+ key: `${clip.id}-${relativeTimeUs}`
215
+ });
216
+ return cachedFrame;
217
+ }
218
+ this.eventBus.emit(MeframeEvent.CacheMiss, {
261
219
  timeUs,
262
220
  level: "L1",
263
221
  key: `${clip.id}-${relativeTimeUs}`
264
222
  });
265
- return cachedFrame;
266
223
  }
267
- this.eventBus.emit(MeframeEvent.CacheMiss, {
268
- timeUs,
269
- level: "L1",
270
- key: `${clip.id}-${relativeTimeUs}`
271
- });
272
224
  if (signal?.aborted) {
273
225
  throw new DOMException("Render aborted", "AbortError");
274
226
  }
275
- const l2Frame = await this.decodeFromL2(relativeTimeUs, clip);
276
- if (l2Frame) {
277
- void this.ensureAudioFromL2(clip.id);
278
- return l2Frame;
279
- }
280
- return null;
281
- }
282
- async ensureAudioFromL2(clipId) {
283
- try {
284
- if (this.cacheManager.hasClipPCM(clipId)) {
285
- return;
286
- }
287
- const hasAudio = await this.cacheManager.hasClipInL2(clipId, "audio");
288
- if (!hasAudio) return;
289
- await this.audioSession.ensureClipAudioFromL2(clipId);
290
- } catch (error) {
291
- console.warn("[Orchestrator] ensureAudioFromL2IfNeeded error:", error);
292
- }
227
+ const resourceFrame = await this.decodeFromResource(clip, relativeTimeUs, timeUs, options);
228
+ return resourceFrame;
293
229
  }
294
- // TODO: move to @ClipSessionManager
295
- async decodeFromL2(timeUs, clip) {
296
- const { id, trackId, startUs } = clip;
297
- const [chunkStream, metadata] = await Promise.all([
298
- this.cacheManager.l2Cache.createReadStream(id, "video"),
299
- this.cacheManager.l2Cache.getClipMetadata(id, "video")
300
- ]);
301
- if (!chunkStream || !metadata?.codec) {
302
- return null;
303
- }
304
- const decoder = new VideoChunkDecoder(`l2-temp-${id}`, {
305
- codec: metadata.codec,
306
- width: metadata.codedWidth,
307
- height: metadata.codedHeight,
308
- description: metadata.description,
309
- hardwareAcceleration: metadata.hardwareAcceleration || "no-preference",
310
- thread: "main"
311
- });
312
- try {
313
- const decodeStream = chunkStream.pipeThrough(decoder.createStream());
314
- let targetFrame = null;
315
- await this.cacheManager.receiveComposedFrames(decodeStream, {
316
- clipId: id,
317
- trackId: trackId || this.compositionModel?.mainTrackId || "main",
318
- fps: this.compositionModel?.fps ?? 30,
319
- clipStartUs: startUs,
320
- onFrame: (info) => {
321
- if (info.timeUs === timeUs) {
322
- targetFrame = this.cacheManager.getFrame(timeUs, id);
323
- }
324
- }
230
+ async preheatNextClip(clip) {
231
+ if (clip.id === this.currentClipId) return;
232
+ this.currentClipId = clip.id;
233
+ const nextClip = this.compositionModel?.getClipsAtTime(
234
+ clip.startUs + clip.durationUs,
235
+ this.compositionModel?.mainTrackId
236
+ )[0];
237
+ if (nextClip && isVideoClip(nextClip)) {
238
+ this.resourceLoader.fetch(nextClip.resourceId, {
239
+ priority: "normal",
240
+ clipId: nextClip.id,
241
+ trackId: nextClip.trackId
325
242
  });
326
- return targetFrame;
327
- } finally {
328
- await decoder.close();
329
243
  }
330
244
  }
331
245
  /**
332
- * Ensure clips are cached using 2-Clip strategy
333
- * Debounced to avoid excessive session activation during fast seek
334
- * @param timeUs - Target time for cache window
335
- * @param immediate - Skip debounce if true (used for initial load)
246
+ * Compose frame from OPFS resource (on-demand decoding)
247
+ * This is the new path for long clips with window caching
336
248
  */
337
- async ensureClipCache(timeUs, immediate = false) {
338
- const executeCache = async () => {
339
- if (!this.compositionModel) return;
340
- const clipIds = this.compositionModel.getClipsToCacheAtTime(timeUs);
341
- if (clipIds.size === 0) return;
342
- await this.clipSessionManager.ensureClips(clipIds);
249
+ async decodeFromResource(clip, relativeTimeUs, globalTimeUs, options) {
250
+ if (!hasResourceId(clip)) return null;
251
+ const resourceId = clip.resourceId;
252
+ const resource = this.compositionModel?.getResource(resourceId);
253
+ const isReady = resource?.state === "ready";
254
+ const fetchOptions = {
255
+ priority: "high",
256
+ clipId: clip.id,
257
+ trackId: clip.trackId
343
258
  };
344
- if (this.ensureCacheDebounceTimer !== null) {
345
- clearTimeout(this.ensureCacheDebounceTimer);
346
- this.ensureCacheDebounceTimer = null;
347
- }
348
- if (immediate) {
349
- return executeCache();
259
+ if (options?.immediate && !isReady) {
260
+ this.resourceLoader.fetch(resourceId, fetchOptions);
261
+ return null;
350
262
  }
351
- return new Promise((resolve) => {
352
- this.ensureCacheDebounceTimer = setTimeout(async () => {
353
- this.ensureCacheDebounceTimer = null;
354
- await executeCache();
355
- resolve();
356
- }, this.ensureCacheDebounceDelay);
263
+ await this.resourceLoader.fetch(resourceId, fetchOptions);
264
+ const session = await OnDemandVideoSession.create({
265
+ clipId: clip.id,
266
+ resourceId,
267
+ targetTimeUs: relativeTimeUs,
268
+ globalTimeUs,
269
+ resourceCache: this.cacheManager.resourceCache,
270
+ mp4IndexCache: this.cacheManager.mp4IndexCache,
271
+ cacheManager: this.cacheManager,
272
+ compositionModel: this.compositionModel,
273
+ fps: this.compositionModel?.fps ?? 30
357
274
  });
275
+ try {
276
+ const DECODE_WINDOW_SIZE = 3e6;
277
+ const windowStart = relativeTimeUs;
278
+ const windowEnd = Math.min(clip.durationUs, relativeTimeUs + DECODE_WINDOW_SIZE);
279
+ await session.decodeWindow(windowStart, windowEnd);
280
+ return this.cacheManager.getFrame(relativeTimeUs, clip.id);
281
+ } catch (error) {
282
+ console.error("[Orchestrator] Error composing from resource:", error);
283
+ return null;
284
+ } finally {
285
+ await session.dispose();
286
+ }
358
287
  }
359
288
  /**
360
289
  * Wait for clip cache to be ready for playback
@@ -375,115 +304,7 @@ class Orchestrator {
375
304
  return this.cacheManager.waitForClipReady(currentClip.id, {
376
305
  minFrameCount: options?.minFrameCount ?? 5,
377
306
  timeoutMs: options?.timeoutMs ?? 5e3
378
- // Don't pass startTimeUs - count all frames in the clip
379
- });
380
- }
381
- // TODO: move to @ClipSessionManager
382
- async renderClipForL2(clipId) {
383
- const sessionId = `${clipId}#l2`;
384
- let session = null;
385
- return new Promise((resolve, reject) => {
386
- this.createSession(sessionId, {
387
- forL2Only: true,
388
- clipId,
389
- onL2Complete: () => {
390
- resolve(true);
391
- },
392
- onL2Error: (error) => {
393
- console.error("[Orchestrator] L2 rendering failed for", clipId, error);
394
- reject(error);
395
- }
396
- }).then((s) => {
397
- session = s;
398
- return session.activate();
399
- }).catch(async (error) => {
400
- if (session) {
401
- await session.dispose();
402
- }
403
- if (error instanceof ResourceConflictError) {
404
- resolve(false);
405
- } else {
406
- reject(error);
407
- }
408
- });
409
- });
410
- }
411
- // TODO: move to @ClipSessionManager
412
- async createSession(sessionId, options) {
413
- const clipId = options?.clipId ?? sessionId;
414
- const clip = this.compositionModel?.findClip(clipId);
415
- if (!clip) {
416
- throw new Error(`Clip ${clipId} not found`);
417
- }
418
- const session = await VideoClipSession.create({
419
- clipId,
420
- sessionId,
421
- forL2Only: options?.forL2Only ?? false,
422
- planner: this.planner,
423
- workerPool: this.workers,
424
- cacheManager: this.cacheManager,
425
- compositionModel: this.compositionModel,
426
- workerConfigs: this.buildWorkerConfigs(),
427
- resourceLoader: this.resourceLoader,
428
- callbacks: {
429
- onComposedStreamReady: async (stream, fps) => {
430
- await this.cacheManager.receiveComposedFrames(stream, {
431
- clipId,
432
- trackId: this.compositionModel.mainTrackId,
433
- fps,
434
- clipStartUs: clip.startUs,
435
- onFrame: () => {
436
- }
437
- });
438
- },
439
- onEncodedStreamReady: async (stream, track) => {
440
- await this.cacheManager.receiveEncodedChunks(stream, clipId, track, {
441
- onComplete: () => {
442
- session.dispose();
443
- if (track === "video" && options?.onL2Complete) {
444
- options.onL2Complete();
445
- }
446
- },
447
- onError: (error) => {
448
- console.error(`[Orchestrator] L2 encode stream error for ${clipId} ${track}:`, error);
449
- session.dispose();
450
- if (options?.onL2Error) {
451
- options.onL2Error(error);
452
- }
453
- }
454
- });
455
- },
456
- onAudioStreamReady: (stream, metadata) => {
457
- if (options?.forL2Only) {
458
- stream.cancel();
459
- } else {
460
- this.audioSession.handleAudioStream(stream, metadata);
461
- }
462
- },
463
- onPipelineReady: async (attachmentResourceIds) => {
464
- const clip2 = this.compositionModel?.findClip(clipId);
465
- if (clip2 && hasResourceId(clip2)) {
466
- await this.resourceLoader.fetch(clip2.resourceId, {
467
- priority: options?.forL2Only ? "low" : "high",
468
- sessionId,
469
- trackId: clip2.trackId,
470
- isMainTrack: true
471
- });
472
- }
473
- if (attachmentResourceIds && attachmentResourceIds.length > 0) {
474
- for (const resourceId of attachmentResourceIds) {
475
- await this.resourceLoader.fetch(resourceId, {
476
- priority: "normal",
477
- sessionId,
478
- isMainTrack: false
479
- });
480
- }
481
- }
482
- }
483
- }
484
307
  });
485
- this.activeClips.add(sessionId);
486
- return session;
487
308
  }
488
309
  async dispose() {
489
310
  if (this.ensureCacheDebounceTimer !== null) {
@@ -491,10 +312,7 @@ class Orchestrator {
491
312
  this.ensureCacheDebounceTimer = null;
492
313
  }
493
314
  this.resourceLoader.dispose();
494
- await this.clipSessionManager.dispose();
495
315
  await this.cacheManager.clear();
496
- this.currentClipId = null;
497
- this.activeClips.clear();
498
316
  this.workers.terminateAll();
499
317
  this.compositionModel = null;
500
318
  this.eventBus.dispose();
@@ -504,6 +322,7 @@ class Orchestrator {
504
322
  const defaultCanvasWidth = config.global?.defaultCanvasWidth ?? 720;
505
323
  const defaultCanvasHeight = config.global?.defaultCanvasHeight ?? 1280;
506
324
  const defaultFps = config.global?.defaultFps ?? 30;
325
+ const targetFps = this.compositionModel?.fps ?? defaultFps;
507
326
  return {
508
327
  videoDemux: {
509
328
  highWaterMark: config.demux?.backpressure?.highWaterMark ?? 10
@@ -516,7 +335,7 @@ class Orchestrator {
516
335
  videoCompose: {
517
336
  width: config.compose?.canvas?.width ?? defaultCanvasWidth,
518
337
  height: config.compose?.canvas?.height ?? defaultCanvasHeight,
519
- fps: config.global?.defaultFps ?? defaultFps,
338
+ fps: targetFps,
520
339
  backgroundColor: config.compose?.canvas?.backgroundColor ?? "#000000",
521
340
  enableSmoothing: config.compose?.visual?.enableSmoothing ?? true,
522
341
  enableHardwareAcceleration: config.compose?.visual?.enableHardwareAcceleration ?? true,
@@ -531,7 +350,7 @@ class Orchestrator {
531
350
  width: config.compose?.canvas?.width || defaultCanvasWidth,
532
351
  height: config.compose?.canvas?.height || defaultCanvasHeight,
533
352
  bitrate: config.encode?.video?.bitrateKbps ? config.encode.video.bitrateKbps * 1e3 : 12e6,
534
- framerate: config.encode?.video?.framerate || defaultFps,
353
+ framerate: targetFps,
535
354
  latencyMode: "quality",
536
355
  bitrateMode: "variable",
537
356
  hardwareAcceleration: "no-preference",
@@ -540,7 +359,121 @@ class Orchestrator {
540
359
  };
541
360
  }
542
361
  async export(model, options) {
543
- return this.muxManager.export(model, options);
362
+ return this.exportScheduler.execute(model, options);
363
+ }
364
+ /**
365
+ * Get render state for real-time composition
366
+ * Returns layers ready for VideoComposer
367
+ */
368
+ async getRenderState(timeUs, options) {
369
+ if (!this.compositionModel) {
370
+ return null;
371
+ }
372
+ const frame = await this.getFrame(timeUs, options);
373
+ if (options?.immediate && !frame) {
374
+ return null;
375
+ }
376
+ const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];
377
+ if (!clip) {
378
+ return null;
379
+ }
380
+ const relativeTimeUs = timeUs - clip.startUs;
381
+ const instructions = this.planner.getInstructions(clip.id);
382
+ if (!instructions) {
383
+ return null;
384
+ }
385
+ const layers = [];
386
+ const activeLayers = instructions.layers.filter((layer) => {
387
+ if (!layer.payload.attachmentId) {
388
+ return true;
389
+ }
390
+ if (layer.status !== "ready") {
391
+ return false;
392
+ }
393
+ return layer.activeRanges.some(
394
+ (range) => relativeTimeUs >= range.startUs && relativeTimeUs < range.endUs
395
+ );
396
+ });
397
+ for (const layerPlan of activeLayers) {
398
+ const layer = this.materializeLayer(layerPlan, clip, relativeTimeUs, timeUs);
399
+ if (layer) {
400
+ layers.push(layer);
401
+ }
402
+ }
403
+ return { layers };
404
+ }
405
+ /**
406
+ * Materialize a serialized layer plan into concrete Layer
407
+ */
408
+ materializeLayer(layerPlan, clip, clipRelativeTimeUs, globalTimeUs) {
409
+ const baseLayer = {
410
+ id: layerPlan.layerId,
411
+ type: layerPlan.type,
412
+ zIndex: layerPlan.zIndex ?? 0,
413
+ visible: true,
414
+ opacity: layerPlan.opacity ?? 1
415
+ };
416
+ if (layerPlan.type === "video" && !layerPlan.payload.attachmentId) {
417
+ const rcFrame = this.cacheManager.getFrame(clipRelativeTimeUs, clip.id);
418
+ if (!rcFrame) {
419
+ console.warn("[Orchestrator] Video frame not found in L1:", clip.id, clipRelativeTimeUs);
420
+ return null;
421
+ }
422
+ return {
423
+ ...baseLayer,
424
+ type: "video",
425
+ rcFrame
426
+ };
427
+ }
428
+ if (layerPlan.type === "text") {
429
+ const payload = layerPlan.payload;
430
+ return {
431
+ ...baseLayer,
432
+ type: "text",
433
+ text: payload.text,
434
+ localeCode: payload.localeCode,
435
+ fontConfig: payload.fontConfig,
436
+ animation: payload.animation,
437
+ wordTimings: payload.wordTimings,
438
+ letterCase: payload.letterCase
439
+ };
440
+ }
441
+ if (layerPlan.type === "image") {
442
+ const payload = layerPlan.payload;
443
+ const resourceId = payload.resourceId;
444
+ const source = this.cacheManager.imageBitmapCache.get(resourceId);
445
+ const imageLayer = {
446
+ ...baseLayer,
447
+ type: "image",
448
+ source,
449
+ attachmentId: payload.attachmentId
450
+ };
451
+ if (payload.targetWidth !== void 0) {
452
+ imageLayer.targetWidth = payload.targetWidth;
453
+ }
454
+ if (payload.targetHeight !== void 0) {
455
+ imageLayer.targetHeight = payload.targetHeight;
456
+ }
457
+ if (payload.animation) {
458
+ const { position, keyframes, overlayClipStartUs } = payload.animation;
459
+ const relativeTimeUs = globalTimeUs - overlayClipStartUs;
460
+ if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {
461
+ return null;
462
+ }
463
+ const rotationRad = 0;
464
+ imageLayer.transform = {
465
+ x: position.x,
466
+ y: position.y,
467
+ scaleX: 1,
468
+ scaleY: 1,
469
+ rotation: rotationRad,
470
+ anchorX: 0.5,
471
+ anchorY: 0.5
472
+ };
473
+ }
474
+ return imageLayer;
475
+ }
476
+ return baseLayer;
544
477
  }
545
478
  }
546
479
  export {