@meframe/core 0.0.29 → 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 (212) 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 +116 -51
  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 +231 -297
  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/LayerRenderer.d.ts +1 -1
  102. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  103. package/dist/stages/compose/LayerRenderer.js +270 -0
  104. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  105. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  106. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  107. package/dist/stages/compose/TransitionProcessor.js +189 -0
  108. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  109. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  110. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  111. package/dist/stages/compose/VideoComposer.js +229 -0
  112. package/dist/stages/compose/VideoComposer.js.map +1 -0
  113. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  114. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  116. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  117. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  118. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  120. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  121. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  122. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  124. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  125. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  126. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  128. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  129. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  130. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  131. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  132. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  133. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  134. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  135. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  136. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  137. package/dist/stages/compose/types.d.ts +2 -1
  138. package/dist/stages/compose/types.d.ts.map +1 -1
  139. package/dist/stages/decode/BaseDecoder.js +0 -3
  140. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  141. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  142. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  143. package/dist/stages/demux/MP4Demuxer.js +281 -0
  144. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  145. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  146. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  147. package/dist/stages/demux/MP4IndexParser.js +416 -0
  148. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  149. package/dist/stages/demux/types.d.ts +48 -0
  150. package/dist/stages/demux/types.d.ts.map +1 -1
  151. package/dist/stages/load/ResourceLoader.d.ts +44 -2
  152. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  153. package/dist/stages/load/ResourceLoader.js +281 -37
  154. package/dist/stages/load/ResourceLoader.js.map +1 -1
  155. package/dist/stages/load/TaskManager.d.ts +6 -2
  156. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  157. package/dist/stages/load/TaskManager.js +27 -4
  158. package/dist/stages/load/TaskManager.js.map +1 -1
  159. package/dist/stages/load/types.d.ts +7 -0
  160. package/dist/stages/load/types.d.ts.map +1 -1
  161. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  162. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  163. package/dist/stages/mux/MP4Muxer.js +24 -13
  164. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  165. package/dist/stages/mux/MuxManager.d.ts +10 -21
  166. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  167. package/dist/stages/mux/MuxManager.js +21 -162
  168. package/dist/stages/mux/MuxManager.js.map +1 -1
  169. package/dist/stages/mux/index.d.ts +0 -1
  170. package/dist/stages/mux/index.d.ts.map +1 -1
  171. package/dist/utils/binary-search.d.ts +12 -4
  172. package/dist/utils/binary-search.d.ts.map +1 -1
  173. package/dist/utils/binary-search.js +52 -6
  174. package/dist/utils/binary-search.js.map +1 -1
  175. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  176. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  178. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  179. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  180. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  182. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  184. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  186. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  187. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  188. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  189. package/dist/workers/worker-manifest.json +5 -5
  190. package/package.json +1 -1
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/OPFSStore.js +0 -131
  193. package/dist/cache/l2/OPFSStore.js.map +0 -1
  194. package/dist/controllers/PreRenderService.d.ts +0 -59
  195. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  196. package/dist/controllers/PreRenderService.js +0 -185
  197. package/dist/controllers/PreRenderService.js.map +0 -1
  198. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  199. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  200. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  201. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.js +0 -158
  203. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  204. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  205. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  206. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  207. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  208. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  209. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  210. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  211. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  212. package/dist/workers/stages/demux/video-demux.worker.B1_wntU4.js.map +0 -1
@@ -1,17 +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";
12
+ import { OnDemandVideoSession } from "./OnDemandVideoSession.js";
13
+ import { ExportScheduler } from "./ExportScheduler.js";
15
14
  class Orchestrator {
16
15
  workers;
17
16
  eventBus;
@@ -21,13 +20,11 @@ class Orchestrator {
21
20
  planner;
22
21
  audioSession;
23
22
  muxManager;
24
- activeClips = /* @__PURE__ */ new Set();
23
+ exportScheduler;
25
24
  isInitialized = false;
26
25
  config = ConfigLoader.getInstance().getConfig();
27
- clipSessionManager;
28
- currentClipId = null;
29
26
  ensureCacheDebounceTimer = null;
30
- ensureCacheDebounceDelay = 150;
27
+ currentClipId = null;
31
28
  events;
32
29
  constructor(config) {
33
30
  this.eventBus = config.eventBus || new EventBus();
@@ -63,13 +60,6 @@ class Orchestrator {
63
60
  },
64
61
  this.eventBus
65
62
  );
66
- this.clipSessionManager = new ClipSessionManager({
67
- maxConcurrent: 2,
68
- factory: {
69
- createSession: (clipId) => this.createSession(clipId)
70
- },
71
- cacheManager: this.cacheManager
72
- });
73
63
  this.audioSession = new GlobalAudioSession({
74
64
  cacheManager: this.cacheManager,
75
65
  workers: this.workers,
@@ -83,26 +73,48 @@ class Orchestrator {
83
73
  this.audioSession,
84
74
  this.config.encode.audio
85
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();
86
88
  }
87
- get workerStatus() {
88
- const status = this.workers.status;
89
- const result = {};
90
- const workerTypes = [
91
- "videoDemux",
92
- "audioDemux",
93
- "videoDecode",
94
- "audioDecode",
95
- "videoCompose",
96
- "audioCompose",
97
- "videoEncode"
98
- ];
99
- for (const type of workerTypes) {
100
- result[type] = status[type] || {
101
- state: "idle",
102
- taskCount: 0
103
- };
104
- }
105
- 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
+ });
106
118
  }
107
119
  async initialize() {
108
120
  if (this.isInitialized) return;
@@ -129,31 +141,25 @@ class Orchestrator {
129
141
  }
130
142
  this.compositionModel = model;
131
143
  this.planner.setModel(model);
132
- this.currentClipId = null;
133
144
  this.eventBus.emit(MeframeEvent.ModelSet, model);
134
145
  this.eventBus.emit(MeframeEvent.CompositionUpdated, {
135
146
  trackCount: model.tracks.length,
136
147
  clipCount: model.tracks.reduce((acc, track) => acc + track.clips.length, 0),
137
148
  durationUs: model.durationUs
138
149
  });
139
- await this.audioSession.activateAllAudioClips();
140
150
  }
141
151
  async applyPatch(patch) {
142
152
  if (!this.compositionModel) {
143
153
  throw new Error("No composition model set");
144
154
  }
145
155
  const affectedClipIds = applyPatch(this.compositionModel, patch);
146
- const clipUpdates = this.planner.applyPatch(patch, affectedClipIds);
156
+ this.planner.applyPatch(patch, affectedClipIds);
147
157
  this.eventBus.emit(MeframeEvent.PatchApplied, {
148
158
  operations: patch.operations.length,
149
159
  affectedClips: Array.from(affectedClipIds)
150
160
  });
151
- for (const update of clipUpdates) {
152
- if (update.type === "remove") {
153
- this.activeClips.delete(update.clipId);
154
- }
155
- this.cacheManager.invalidateClip(update.clipId);
156
- await this.clipSessionManager.handlePlannerUpdate(update.clipId, update);
161
+ for (const clipId of affectedClipIds) {
162
+ this.cacheManager.invalidateClip(clipId);
157
163
  }
158
164
  const reactivatedAudioClips = [];
159
165
  const reactivatedVideoClips = [];
@@ -184,62 +190,10 @@ class Orchestrator {
184
190
  if (state !== "ready") {
185
191
  return;
186
192
  }
187
- const resource = this.compositionModel.getResource(resourceId);
188
- if (!resource) {
189
- return;
190
- }
191
- if (resource.type === "video" || resource.type === "audio") {
192
- return;
193
- }
194
- const clipIds = this.compositionModel.getClipIdsByResourceId(resourceId);
195
- for (const clipId of clipIds) {
196
- if (!this.clipSessionManager.isClipActive(clipId)) {
197
- continue;
198
- }
199
- const clip = this.compositionModel.findClip(clipId);
200
- if (!clip) {
201
- continue;
202
- }
203
- const instructions = this.planner.getInstructions(clipId);
204
- if (!instructions) {
205
- continue;
206
- }
207
- const session = this.clipSessionManager.getSession(clipId);
208
- const composeWorker = session?.composeWorker;
209
- if (composeWorker) {
210
- composeWorker.send("install_instructions", instructions);
211
- }
212
- }
213
- }
214
- async restartWorker(type, clipId) {
215
- const clipLocalTypes = [
216
- "videoDemux",
217
- "audioDemux",
218
- "videoDecode",
219
- "audioDecode",
220
- "videoCompose",
221
- "videoEncode"
222
- ];
223
- if (clipLocalTypes.includes(type) && !clipId) {
224
- throw new Error(`clipId required for restarting ${type} worker`);
225
- }
226
- this.workers.terminate(type, clipId);
227
- const worker = await this.workers.getOrCreate(type, clipId);
228
- this.eventBus.emit(MeframeEvent.WorkerRestarted, {
229
- type,
230
- workerId: worker.getWorkerId(),
231
- reason: "Manual restart"
232
- });
233
- if (clipId) {
234
- const session = this.clipSessionManager.getSession(clipId);
235
- if (session) {
236
- await session.activate();
237
- }
238
- }
239
193
  }
240
- async renderFrame(timeUs, options) {
194
+ async getFrame(timeUs, options) {
241
195
  const signal = options?.signal;
242
- const immediate = options?.immediate ?? true;
196
+ const preheat = options?.preheat ?? false;
243
197
  if (!this.compositionModel) {
244
198
  throw new Error("No composition model set");
245
199
  }
@@ -247,112 +201,89 @@ class Orchestrator {
247
201
  if (!clip) {
248
202
  return null;
249
203
  }
250
- if (this.currentClipId !== clip.id) {
251
- this.currentClipId = clip.id;
252
- void this.ensureClipCache(timeUs, immediate);
253
- }
254
204
  let relativeTimeUs = options?.relativeTimeUs ?? timeUs - clip.startUs;
255
205
  relativeTimeUs = Math.min(relativeTimeUs, clip.durationUs - 1);
256
- const cachedFrame = this.cacheManager.getFrame(relativeTimeUs, clip.id);
257
- if (cachedFrame) {
258
- 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, {
259
219
  timeUs,
260
220
  level: "L1",
261
221
  key: `${clip.id}-${relativeTimeUs}`
262
222
  });
263
- return cachedFrame;
264
223
  }
265
- this.eventBus.emit(MeframeEvent.CacheMiss, {
266
- timeUs,
267
- level: "L1",
268
- key: `${clip.id}-${relativeTimeUs}`
269
- });
270
224
  if (signal?.aborted) {
271
225
  throw new DOMException("Render aborted", "AbortError");
272
226
  }
273
- const l2Frame = await this.decodeFromL2(relativeTimeUs, clip);
274
- if (l2Frame) {
275
- void this.ensureAudioFromL2(clip.id);
276
- return l2Frame;
277
- }
278
- return null;
279
- }
280
- async ensureAudioFromL2(clipId) {
281
- try {
282
- if (this.cacheManager.hasClipPCM(clipId)) {
283
- return;
284
- }
285
- const hasAudio = await this.cacheManager.hasClipInL2(clipId, "audio");
286
- if (!hasAudio) return;
287
- await this.audioSession.ensureClipAudioFromL2(clipId);
288
- } catch (error) {
289
- console.warn("[Orchestrator] ensureAudioFromL2IfNeeded error:", error);
290
- }
227
+ const resourceFrame = await this.decodeFromResource(clip, relativeTimeUs, timeUs, options);
228
+ return resourceFrame;
291
229
  }
292
- // TODO: move to @ClipSessionManager
293
- async decodeFromL2(timeUs, clip) {
294
- const { id, trackId, startUs } = clip;
295
- const [chunkStream, metadata] = await Promise.all([
296
- this.cacheManager.l2Cache.createReadStream(id, "video"),
297
- this.cacheManager.l2Cache.getClipMetadata(id, "video")
298
- ]);
299
- if (!chunkStream || !metadata?.codec) {
300
- return null;
301
- }
302
- const decoder = new VideoChunkDecoder(`l2-temp-${id}`, {
303
- codec: metadata.codec,
304
- width: metadata.codedWidth,
305
- height: metadata.codedHeight,
306
- description: metadata.description,
307
- hardwareAcceleration: metadata.hardwareAcceleration || "no-preference",
308
- thread: "main"
309
- });
310
- try {
311
- const decodeStream = chunkStream.pipeThrough(decoder.createStream());
312
- let targetFrame = null;
313
- await this.cacheManager.receiveComposedFrames(decodeStream, {
314
- clipId: id,
315
- trackId: trackId || this.compositionModel?.mainTrackId || "main",
316
- fps: this.compositionModel?.fps ?? 30,
317
- clipStartUs: startUs,
318
- onFrame: (info) => {
319
- if (info.timeUs === timeUs) {
320
- targetFrame = this.cacheManager.getFrame(timeUs, id);
321
- }
322
- }
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
323
242
  });
324
- return targetFrame;
325
- } finally {
326
- await decoder.close();
327
243
  }
328
244
  }
329
245
  /**
330
- * Ensure clips are cached using 2-Clip strategy
331
- * Debounced to avoid excessive session activation during fast seek
332
- * @param timeUs - Target time for cache window
333
- * @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
334
248
  */
335
- async ensureClipCache(timeUs, immediate = false) {
336
- const executeCache = async () => {
337
- if (!this.compositionModel) return;
338
- const clipIds = this.compositionModel.getClipsToCacheAtTime(timeUs);
339
- if (clipIds.size === 0) return;
340
- 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
341
258
  };
342
- if (this.ensureCacheDebounceTimer !== null) {
343
- clearTimeout(this.ensureCacheDebounceTimer);
344
- this.ensureCacheDebounceTimer = null;
345
- }
346
- if (immediate) {
347
- return executeCache();
259
+ if (options?.immediate && !isReady) {
260
+ this.resourceLoader.fetch(resourceId, fetchOptions);
261
+ return null;
348
262
  }
349
- return new Promise((resolve) => {
350
- this.ensureCacheDebounceTimer = setTimeout(async () => {
351
- this.ensureCacheDebounceTimer = null;
352
- await executeCache();
353
- resolve();
354
- }, 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
355
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
+ }
356
287
  }
357
288
  /**
358
289
  * Wait for clip cache to be ready for playback
@@ -373,115 +304,7 @@ class Orchestrator {
373
304
  return this.cacheManager.waitForClipReady(currentClip.id, {
374
305
  minFrameCount: options?.minFrameCount ?? 5,
375
306
  timeoutMs: options?.timeoutMs ?? 5e3
376
- // Don't pass startTimeUs - count all frames in the clip
377
- });
378
- }
379
- // TODO: move to @ClipSessionManager
380
- async renderClipForL2(clipId) {
381
- const sessionId = `${clipId}#l2`;
382
- let session = null;
383
- return new Promise((resolve, reject) => {
384
- this.createSession(sessionId, {
385
- forL2Only: true,
386
- clipId,
387
- onL2Complete: () => {
388
- resolve(true);
389
- },
390
- onL2Error: (error) => {
391
- console.error("[Orchestrator] L2 rendering failed for", clipId, error);
392
- reject(error);
393
- }
394
- }).then((s) => {
395
- session = s;
396
- return session.activate();
397
- }).catch(async (error) => {
398
- if (session) {
399
- await session.dispose();
400
- }
401
- if (error instanceof ResourceConflictError) {
402
- resolve(false);
403
- } else {
404
- reject(error);
405
- }
406
- });
407
- });
408
- }
409
- // TODO: move to @ClipSessionManager
410
- async createSession(sessionId, options) {
411
- const clipId = options?.clipId ?? sessionId;
412
- const clip = this.compositionModel?.findClip(clipId);
413
- if (!clip) {
414
- throw new Error(`Clip ${clipId} not found`);
415
- }
416
- const session = await VideoClipSession.create({
417
- clipId,
418
- sessionId,
419
- forL2Only: options?.forL2Only ?? false,
420
- planner: this.planner,
421
- workerPool: this.workers,
422
- cacheManager: this.cacheManager,
423
- compositionModel: this.compositionModel,
424
- workerConfigs: this.buildWorkerConfigs(),
425
- resourceLoader: this.resourceLoader,
426
- callbacks: {
427
- onComposedStreamReady: async (stream, fps) => {
428
- await this.cacheManager.receiveComposedFrames(stream, {
429
- clipId,
430
- trackId: this.compositionModel.mainTrackId,
431
- fps,
432
- clipStartUs: clip.startUs,
433
- onFrame: () => {
434
- }
435
- });
436
- },
437
- onEncodedStreamReady: async (stream, track) => {
438
- await this.cacheManager.receiveEncodedChunks(stream, clipId, track, {
439
- onComplete: () => {
440
- session.dispose();
441
- if (track === "video" && options?.onL2Complete) {
442
- options.onL2Complete();
443
- }
444
- },
445
- onError: (error) => {
446
- console.error(`[Orchestrator] L2 encode stream error for ${clipId} ${track}:`, error);
447
- session.dispose();
448
- if (options?.onL2Error) {
449
- options.onL2Error(error);
450
- }
451
- }
452
- });
453
- },
454
- onAudioStreamReady: (stream, metadata) => {
455
- if (options?.forL2Only) {
456
- stream.cancel();
457
- } else {
458
- this.audioSession.handleAudioStream(stream, metadata);
459
- }
460
- },
461
- onPipelineReady: async (attachmentResourceIds) => {
462
- const clip2 = this.compositionModel?.findClip(clipId);
463
- if (clip2 && hasResourceId(clip2)) {
464
- await this.resourceLoader.fetch(clip2.resourceId, {
465
- priority: options?.forL2Only ? "low" : "high",
466
- sessionId,
467
- trackId: clip2.trackId,
468
- isMainTrack: true
469
- });
470
- }
471
- if (attachmentResourceIds && attachmentResourceIds.length > 0) {
472
- for (const resourceId of attachmentResourceIds) {
473
- await this.resourceLoader.fetch(resourceId, {
474
- priority: "normal",
475
- sessionId,
476
- isMainTrack: false
477
- });
478
- }
479
- }
480
- }
481
- }
482
307
  });
483
- this.activeClips.add(sessionId);
484
- return session;
485
308
  }
486
309
  async dispose() {
487
310
  if (this.ensureCacheDebounceTimer !== null) {
@@ -489,10 +312,7 @@ class Orchestrator {
489
312
  this.ensureCacheDebounceTimer = null;
490
313
  }
491
314
  this.resourceLoader.dispose();
492
- await this.clipSessionManager.dispose();
493
315
  await this.cacheManager.clear();
494
- this.currentClipId = null;
495
- this.activeClips.clear();
496
316
  this.workers.terminateAll();
497
317
  this.compositionModel = null;
498
318
  this.eventBus.dispose();
@@ -539,7 +359,121 @@ class Orchestrator {
539
359
  };
540
360
  }
541
361
  async export(model, options) {
542
- 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;
543
477
  }
544
478
  }
545
479
  export {