@meframe/core 0.0.29 → 0.0.30

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 (218) hide show
  1. package/dist/Meframe.d.ts +2 -30
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +7 -118
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +42 -68
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +224 -188
  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/L2OPFSStore.d.ts +37 -0
  15. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  16. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  17. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  18. package/dist/cache/resource/AudioSampleCache.js +69 -0
  19. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  20. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  21. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  22. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  23. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  24. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  25. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  26. package/dist/cache/resource/MP4IndexCache.js +104 -0
  27. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  28. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  29. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  30. package/dist/cache/resource/ResourceCache.js +92 -0
  31. package/dist/cache/resource/ResourceCache.js.map +1 -0
  32. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  33. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  34. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  35. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  36. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  37. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  38. package/dist/cache/storage/opfs/types.d.ts +16 -0
  39. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  40. package/dist/config/defaults.d.ts.map +1 -1
  41. package/dist/config/defaults.js +21 -2
  42. package/dist/config/defaults.js.map +1 -1
  43. package/dist/config/types.d.ts +28 -0
  44. package/dist/config/types.d.ts.map +1 -1
  45. package/dist/controllers/ExportController.d.ts +16 -0
  46. package/dist/controllers/ExportController.d.ts.map +1 -0
  47. package/dist/controllers/ExportController.js +44 -0
  48. package/dist/controllers/ExportController.js.map +1 -0
  49. package/dist/controllers/PlaybackController.d.ts +28 -4
  50. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  51. package/dist/controllers/PlaybackController.js +115 -51
  52. package/dist/controllers/PlaybackController.js.map +1 -1
  53. package/dist/controllers/index.d.ts +2 -3
  54. package/dist/controllers/index.d.ts.map +1 -1
  55. package/dist/controllers/types.d.ts +0 -28
  56. package/dist/controllers/types.d.ts.map +1 -1
  57. package/dist/event/events.d.ts +8 -0
  58. package/dist/event/events.d.ts.map +1 -1
  59. package/dist/event/events.js +1 -0
  60. package/dist/event/events.js.map +1 -1
  61. package/dist/model/CompositionModel.d.ts.map +1 -1
  62. package/dist/model/CompositionModel.js +11 -6
  63. package/dist/model/CompositionModel.js.map +1 -1
  64. package/dist/model/RcFrame.d.ts +2 -0
  65. package/dist/model/RcFrame.d.ts.map +1 -1
  66. package/dist/model/RcFrame.js +3 -0
  67. package/dist/model/RcFrame.js.map +1 -1
  68. package/dist/model/types.d.ts +0 -1
  69. package/dist/model/types.d.ts.map +1 -1
  70. package/dist/model/types.js.map +1 -1
  71. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  72. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  73. package/dist/orchestrator/ExportScheduler.js +241 -0
  74. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  75. package/dist/orchestrator/GlobalAudioSession.d.ts +24 -9
  76. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  77. package/dist/orchestrator/GlobalAudioSession.js +149 -152
  78. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  79. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  80. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  81. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  82. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  83. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  84. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  85. package/dist/orchestrator/Orchestrator.js +244 -312
  86. package/dist/orchestrator/Orchestrator.js.map +1 -1
  87. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  88. package/dist/orchestrator/VideoClipSession.js +3 -15
  89. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  90. package/dist/orchestrator/index.d.ts +0 -1
  91. package/dist/orchestrator/index.d.ts.map +1 -1
  92. package/dist/orchestrator/types.d.ts +4 -4
  93. package/dist/orchestrator/types.d.ts.map +1 -1
  94. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  95. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  96. package/dist/stages/compose/FilterProcessor.js +226 -0
  97. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  98. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  99. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  100. package/dist/stages/compose/LayerRenderer.js +270 -0
  101. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  102. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  103. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  104. package/dist/stages/compose/TransitionProcessor.js +189 -0
  105. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  106. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  107. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  108. package/dist/stages/compose/VideoComposer.js +229 -0
  109. package/dist/stages/compose/VideoComposer.js.map +1 -0
  110. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  111. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  112. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  113. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  114. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  116. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  117. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  118. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  120. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  121. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  122. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  124. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  125. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  126. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  128. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  129. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  130. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  131. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  132. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  133. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  134. package/dist/stages/compose/types.d.ts +2 -1
  135. package/dist/stages/compose/types.d.ts.map +1 -1
  136. package/dist/stages/decode/BaseDecoder.js +0 -3
  137. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  138. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  139. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  140. package/dist/stages/demux/MP4Demuxer.js +281 -0
  141. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  142. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  143. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  144. package/dist/stages/demux/MP4IndexParser.js +416 -0
  145. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  146. package/dist/stages/demux/types.d.ts +48 -0
  147. package/dist/stages/demux/types.d.ts.map +1 -1
  148. package/dist/stages/load/ResourceLoader.d.ts +50 -15
  149. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  150. package/dist/stages/load/ResourceLoader.js +297 -80
  151. package/dist/stages/load/ResourceLoader.js.map +1 -1
  152. package/dist/stages/load/TaskManager.d.ts +6 -2
  153. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  154. package/dist/stages/load/TaskManager.js +27 -4
  155. package/dist/stages/load/TaskManager.js.map +1 -1
  156. package/dist/stages/load/index.d.ts +0 -1
  157. package/dist/stages/load/index.d.ts.map +1 -1
  158. package/dist/stages/load/types.d.ts +9 -9
  159. package/dist/stages/load/types.d.ts.map +1 -1
  160. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  161. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  162. package/dist/stages/mux/MP4Muxer.js +24 -13
  163. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  164. package/dist/stages/mux/MuxManager.d.ts +10 -21
  165. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  166. package/dist/stages/mux/MuxManager.js +21 -162
  167. package/dist/stages/mux/MuxManager.js.map +1 -1
  168. package/dist/stages/mux/index.d.ts +0 -1
  169. package/dist/stages/mux/index.d.ts.map +1 -1
  170. package/dist/utils/binary-search.d.ts +12 -4
  171. package/dist/utils/binary-search.d.ts.map +1 -1
  172. package/dist/utils/binary-search.js +52 -6
  173. package/dist/utils/binary-search.js.map +1 -1
  174. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  175. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  176. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  178. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  179. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  180. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  182. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  184. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  186. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  187. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  188. package/dist/workers/worker-manifest.json +5 -5
  189. package/package.json +1 -1
  190. package/dist/cache/l2/IndexedDBStore.js +0 -180
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/L2Cache.js +0 -329
  193. package/dist/cache/l2/L2Cache.js.map +0 -1
  194. package/dist/cache/l2/OPFSStore.js +0 -131
  195. package/dist/cache/l2/OPFSStore.js.map +0 -1
  196. package/dist/controllers/PreRenderService.d.ts +0 -59
  197. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  198. package/dist/controllers/PreRenderService.js +0 -185
  199. package/dist/controllers/PreRenderService.js.map +0 -1
  200. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  201. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  203. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  204. package/dist/orchestrator/ClipSessionManager.js +0 -158
  205. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  206. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  207. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  208. package/dist/stages/load/EventHandlers.d.ts +0 -26
  209. package/dist/stages/load/EventHandlers.d.ts.map +0 -1
  210. package/dist/stages/load/EventHandlers.js +0 -42
  211. package/dist/stages/load/EventHandlers.js.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/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  217. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  218. 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();
@@ -40,15 +37,6 @@ class Orchestrator {
40
37
  workerPath: config.workerPath,
41
38
  workerExtension: config.workerExtension
42
39
  });
43
- this.resourceLoader = new ResourceLoader({
44
- orchestrator: this,
45
- eventBus: this.eventBus,
46
- config: {
47
- maxConcurrent: config.maxWorkers || this.config.load?.retry?.maxAttempts || 4
48
- },
49
- onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state)
50
- });
51
- this.planner = new CompositionPlanner();
52
40
  const cacheConfig = config.cacheConfig || this.config.cache;
53
41
  this.cacheManager = new CacheManager(
54
42
  {
@@ -56,26 +44,27 @@ class Orchestrator {
56
44
  maxMemoryMB: cacheConfig?.l1Size || cacheConfig?.l1?.maxMemoryMB || 1024,
57
45
  maxGOPs: this.config.decode?.video?.maxGOPs || 4
58
46
  },
59
- l2: {
60
- maxSizeMB: cacheConfig?.l2Size || cacheConfig?.l2?.maxSizeMB || 2048,
47
+ resource: {
61
48
  projectId: config.projectId || this.config.global.projectId || "default"
62
49
  }
63
50
  },
64
51
  this.eventBus
65
52
  );
66
- this.clipSessionManager = new ClipSessionManager({
67
- maxConcurrent: 2,
68
- factory: {
69
- createSession: (clipId) => this.createSession(clipId)
53
+ this.resourceLoader = new ResourceLoader({
54
+ cacheManager: this.cacheManager,
55
+ workerPool: this.workers,
56
+ eventBus: this.eventBus,
57
+ config: {
58
+ maxConcurrent: config.maxWorkers || this.config.load?.retry?.maxAttempts || 4
70
59
  },
71
- cacheManager: this.cacheManager
60
+ onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state)
72
61
  });
62
+ this.planner = new CompositionPlanner();
73
63
  this.audioSession = new GlobalAudioSession({
74
64
  cacheManager: this.cacheManager,
75
- workers: this.workers,
65
+ workerPool: this.workers,
76
66
  resourceLoader: this.resourceLoader,
77
67
  eventBus: this.eventBus,
78
- getModel: () => this.compositionModel,
79
68
  buildWorkerConfigs: () => this.buildWorkerConfigs()
80
69
  });
81
70
  this.muxManager = new MuxManager(
@@ -83,26 +72,48 @@ class Orchestrator {
83
72
  this.audioSession,
84
73
  this.config.encode.audio
85
74
  );
75
+ this.exportScheduler = new ExportScheduler({
76
+ workerPool: this.workers,
77
+ planner: this.planner,
78
+ cacheManager: this.cacheManager,
79
+ resourceLoader: this.resourceLoader,
80
+ muxManager: this.muxManager,
81
+ audioSession: this.audioSession,
82
+ workerConfigsProvider: () => this.buildWorkerConfigs(),
83
+ eventBus: this.eventBus
84
+ });
85
+ this.setupResourceFirstFrameHandler();
86
+ this.setupPreloadHandlers();
86
87
  }
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;
88
+ setupResourceFirstFrameHandler() {
89
+ this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {
90
+ const { resourceId, clipId, index, chunks } = payload;
91
+ if (!this.compositionModel) return;
92
+ const clip = this.compositionModel.findClip(clipId);
93
+ if (!clip || !clip.trackId) return;
94
+ if (clip.startUs === 0) {
95
+ const fps = this.compositionModel.fps ?? 30;
96
+ await OnDemandVideoSession.decodeAndCacheFirstFrame(
97
+ resourceId,
98
+ chunks,
99
+ index,
100
+ clip,
101
+ this.cacheManager,
102
+ fps
103
+ );
104
+ }
105
+ });
106
+ }
107
+ setupPreloadHandlers() {
108
+ this.eventBus.on(MeframeEvent.PlaybackPlay, () => {
109
+ this.resourceLoader.setPreloadingEnabled(false);
110
+ });
111
+ this.eventBus.on(MeframeEvent.PlaybackPause, () => {
112
+ this.resourceLoader.setPreloadingEnabled(true);
113
+ });
114
+ this.eventBus.on(MeframeEvent.PlaybackStop, () => {
115
+ this.resourceLoader.setPreloadingEnabled(true);
116
+ });
106
117
  }
107
118
  async initialize() {
108
119
  if (this.isInitialized) return;
@@ -120,40 +131,33 @@ class Orchestrator {
120
131
  this.eventBus.once(event, handler);
121
132
  }
122
133
  async setCompositionModel(model, options) {
123
- const { clearL1Cache = true, clearL2Cache = false } = options || {};
134
+ const { clearL1Cache = true } = options || {};
124
135
  if (clearL1Cache) {
125
136
  this.cacheManager.clearL1Cache();
126
137
  }
127
- if (clearL2Cache) {
128
- await this.cacheManager.clearL2Cache();
129
- }
130
138
  this.compositionModel = model;
131
139
  this.planner.setModel(model);
132
- this.currentClipId = null;
140
+ await this.resourceLoader.setModel(model);
141
+ this.audioSession.setModel(model);
133
142
  this.eventBus.emit(MeframeEvent.ModelSet, model);
134
143
  this.eventBus.emit(MeframeEvent.CompositionUpdated, {
135
144
  trackCount: model.tracks.length,
136
145
  clipCount: model.tracks.reduce((acc, track) => acc + track.clips.length, 0),
137
146
  durationUs: model.durationUs
138
147
  });
139
- await this.audioSession.activateAllAudioClips();
140
148
  }
141
149
  async applyPatch(patch) {
142
150
  if (!this.compositionModel) {
143
151
  throw new Error("No composition model set");
144
152
  }
145
153
  const affectedClipIds = applyPatch(this.compositionModel, patch);
146
- const clipUpdates = this.planner.applyPatch(patch, affectedClipIds);
154
+ this.planner.applyPatch(patch, affectedClipIds);
147
155
  this.eventBus.emit(MeframeEvent.PatchApplied, {
148
156
  operations: patch.operations.length,
149
157
  affectedClips: Array.from(affectedClipIds)
150
158
  });
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);
159
+ for (const clipId of affectedClipIds) {
160
+ this.cacheManager.invalidateClip(clipId);
157
161
  }
158
162
  const reactivatedAudioClips = [];
159
163
  const reactivatedVideoClips = [];
@@ -184,62 +188,10 @@ class Orchestrator {
184
188
  if (state !== "ready") {
185
189
  return;
186
190
  }
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
191
  }
240
- async renderFrame(timeUs, options) {
192
+ async getFrame(timeUs, options) {
241
193
  const signal = options?.signal;
242
- const immediate = options?.immediate ?? true;
194
+ const preheat = options?.preheat ?? false;
243
195
  if (!this.compositionModel) {
244
196
  throw new Error("No composition model set");
245
197
  }
@@ -247,112 +199,89 @@ class Orchestrator {
247
199
  if (!clip) {
248
200
  return null;
249
201
  }
250
- if (this.currentClipId !== clip.id) {
251
- this.currentClipId = clip.id;
252
- void this.ensureClipCache(timeUs, immediate);
253
- }
254
202
  let relativeTimeUs = options?.relativeTimeUs ?? timeUs - clip.startUs;
255
203
  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, {
204
+ if (!preheat) {
205
+ this.cacheManager.setWindow(timeUs);
206
+ const cachedFrame = this.cacheManager.getFrame(relativeTimeUs, clip.id);
207
+ if (cachedFrame) {
208
+ this.preheatNextClip(clip);
209
+ this.eventBus.emit(MeframeEvent.CacheHit, {
210
+ timeUs,
211
+ level: "L1",
212
+ key: `${clip.id}-${relativeTimeUs}`
213
+ });
214
+ return cachedFrame;
215
+ }
216
+ this.eventBus.emit(MeframeEvent.CacheMiss, {
259
217
  timeUs,
260
218
  level: "L1",
261
219
  key: `${clip.id}-${relativeTimeUs}`
262
220
  });
263
- return cachedFrame;
264
221
  }
265
- this.eventBus.emit(MeframeEvent.CacheMiss, {
266
- timeUs,
267
- level: "L1",
268
- key: `${clip.id}-${relativeTimeUs}`
269
- });
270
222
  if (signal?.aborted) {
271
223
  throw new DOMException("Render aborted", "AbortError");
272
224
  }
273
- const l2Frame = await this.decodeFromL2(relativeTimeUs, clip);
274
- if (l2Frame) {
275
- void this.ensureAudioFromL2(clip.id);
276
- return l2Frame;
277
- }
278
- return null;
225
+ const resourceFrame = await this.decodeFromResource(clip, relativeTimeUs, timeUs, options);
226
+ return resourceFrame;
279
227
  }
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
- }
291
- }
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
- }
228
+ async preheatNextClip(clip) {
229
+ if (clip.id === this.currentClipId) return;
230
+ this.currentClipId = clip.id;
231
+ const nextClip = this.compositionModel?.getClipsAtTime(
232
+ clip.startUs + clip.durationUs,
233
+ this.compositionModel?.mainTrackId
234
+ )[0];
235
+ if (nextClip && isVideoClip(nextClip)) {
236
+ this.resourceLoader.fetch(nextClip.resourceId, {
237
+ priority: "normal",
238
+ clipId: nextClip.id,
239
+ trackId: nextClip.trackId
323
240
  });
324
- return targetFrame;
325
- } finally {
326
- await decoder.close();
327
241
  }
328
242
  }
329
243
  /**
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)
244
+ * Compose frame from OPFS resource (on-demand decoding)
245
+ * This is the new path for long clips with window caching
334
246
  */
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);
247
+ async decodeFromResource(clip, relativeTimeUs, globalTimeUs, options) {
248
+ if (!hasResourceId(clip)) return null;
249
+ const resourceId = clip.resourceId;
250
+ const resource = this.compositionModel?.getResource(resourceId);
251
+ const isReady = resource?.state === "ready";
252
+ const fetchOptions = {
253
+ priority: "high",
254
+ clipId: clip.id,
255
+ trackId: clip.trackId
341
256
  };
342
- if (this.ensureCacheDebounceTimer !== null) {
343
- clearTimeout(this.ensureCacheDebounceTimer);
344
- this.ensureCacheDebounceTimer = null;
345
- }
346
- if (immediate) {
347
- return executeCache();
257
+ if (options?.immediate && !isReady) {
258
+ this.resourceLoader.fetch(resourceId, fetchOptions);
259
+ return null;
348
260
  }
349
- return new Promise((resolve) => {
350
- this.ensureCacheDebounceTimer = setTimeout(async () => {
351
- this.ensureCacheDebounceTimer = null;
352
- await executeCache();
353
- resolve();
354
- }, this.ensureCacheDebounceDelay);
261
+ await this.resourceLoader.fetch(resourceId, fetchOptions);
262
+ const session = await OnDemandVideoSession.create({
263
+ clipId: clip.id,
264
+ resourceId,
265
+ targetTimeUs: relativeTimeUs,
266
+ globalTimeUs,
267
+ resourceCache: this.cacheManager.resourceCache,
268
+ mp4IndexCache: this.cacheManager.mp4IndexCache,
269
+ cacheManager: this.cacheManager,
270
+ compositionModel: this.compositionModel,
271
+ fps: this.compositionModel?.fps ?? 30
355
272
  });
273
+ try {
274
+ const DECODE_WINDOW_SIZE = 3e6;
275
+ const windowStart = relativeTimeUs;
276
+ const windowEnd = Math.min(clip.durationUs, relativeTimeUs + DECODE_WINDOW_SIZE);
277
+ await session.decodeWindow(windowStart, windowEnd);
278
+ return this.cacheManager.getFrame(relativeTimeUs, clip.id);
279
+ } catch (error) {
280
+ console.error("[Orchestrator] Error composing from resource:", error);
281
+ return null;
282
+ } finally {
283
+ await session.dispose();
284
+ }
356
285
  }
357
286
  /**
358
287
  * Wait for clip cache to be ready for playback
@@ -373,115 +302,7 @@ class Orchestrator {
373
302
  return this.cacheManager.waitForClipReady(currentClip.id, {
374
303
  minFrameCount: options?.minFrameCount ?? 5,
375
304
  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
305
  });
483
- this.activeClips.add(sessionId);
484
- return session;
485
306
  }
486
307
  async dispose() {
487
308
  if (this.ensureCacheDebounceTimer !== null) {
@@ -489,10 +310,7 @@ class Orchestrator {
489
310
  this.ensureCacheDebounceTimer = null;
490
311
  }
491
312
  this.resourceLoader.dispose();
492
- await this.clipSessionManager.dispose();
493
313
  await this.cacheManager.clear();
494
- this.currentClipId = null;
495
- this.activeClips.clear();
496
314
  this.workers.terminateAll();
497
315
  this.compositionModel = null;
498
316
  this.eventBus.dispose();
@@ -539,7 +357,121 @@ class Orchestrator {
539
357
  };
540
358
  }
541
359
  async export(model, options) {
542
- return this.muxManager.export(model, options);
360
+ return this.exportScheduler.execute(model, options);
361
+ }
362
+ /**
363
+ * Get render state for real-time composition
364
+ * Returns layers ready for VideoComposer
365
+ */
366
+ async getRenderState(timeUs, options) {
367
+ if (!this.compositionModel) {
368
+ return null;
369
+ }
370
+ const frame = await this.getFrame(timeUs, options);
371
+ if (options?.immediate && !frame) {
372
+ return null;
373
+ }
374
+ const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];
375
+ if (!clip) {
376
+ return null;
377
+ }
378
+ const relativeTimeUs = timeUs - clip.startUs;
379
+ const instructions = this.planner.getInstructions(clip.id);
380
+ if (!instructions) {
381
+ return null;
382
+ }
383
+ const layers = [];
384
+ const activeLayers = instructions.layers.filter((layer) => {
385
+ if (!layer.payload.attachmentId) {
386
+ return true;
387
+ }
388
+ if (layer.status !== "ready") {
389
+ return false;
390
+ }
391
+ return layer.activeRanges.some(
392
+ (range) => relativeTimeUs >= range.startUs && relativeTimeUs < range.endUs
393
+ );
394
+ });
395
+ for (const layerPlan of activeLayers) {
396
+ const layer = this.materializeLayer(layerPlan, clip, relativeTimeUs, timeUs);
397
+ if (layer) {
398
+ layers.push(layer);
399
+ }
400
+ }
401
+ return { layers };
402
+ }
403
+ /**
404
+ * Materialize a serialized layer plan into concrete Layer
405
+ */
406
+ materializeLayer(layerPlan, clip, clipRelativeTimeUs, globalTimeUs) {
407
+ const baseLayer = {
408
+ id: layerPlan.layerId,
409
+ type: layerPlan.type,
410
+ zIndex: layerPlan.zIndex ?? 0,
411
+ visible: true,
412
+ opacity: layerPlan.opacity ?? 1
413
+ };
414
+ if (layerPlan.type === "video" && !layerPlan.payload.attachmentId) {
415
+ const rcFrame = this.cacheManager.getFrame(clipRelativeTimeUs, clip.id);
416
+ if (!rcFrame) {
417
+ console.warn("[Orchestrator] Video frame not found in L1:", clip.id, clipRelativeTimeUs);
418
+ return null;
419
+ }
420
+ return {
421
+ ...baseLayer,
422
+ type: "video",
423
+ rcFrame
424
+ };
425
+ }
426
+ if (layerPlan.type === "text") {
427
+ const payload = layerPlan.payload;
428
+ return {
429
+ ...baseLayer,
430
+ type: "text",
431
+ text: payload.text,
432
+ localeCode: payload.localeCode,
433
+ fontConfig: payload.fontConfig,
434
+ animation: payload.animation,
435
+ wordTimings: payload.wordTimings,
436
+ letterCase: payload.letterCase
437
+ };
438
+ }
439
+ if (layerPlan.type === "image") {
440
+ const payload = layerPlan.payload;
441
+ const resourceId = payload.resourceId;
442
+ const source = this.cacheManager.imageBitmapCache.get(resourceId);
443
+ const imageLayer = {
444
+ ...baseLayer,
445
+ type: "image",
446
+ source,
447
+ attachmentId: payload.attachmentId
448
+ };
449
+ if (payload.targetWidth !== void 0) {
450
+ imageLayer.targetWidth = payload.targetWidth;
451
+ }
452
+ if (payload.targetHeight !== void 0) {
453
+ imageLayer.targetHeight = payload.targetHeight;
454
+ }
455
+ if (payload.animation) {
456
+ const { position, keyframes, overlayClipStartUs } = payload.animation;
457
+ const relativeTimeUs = globalTimeUs - overlayClipStartUs;
458
+ if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {
459
+ return null;
460
+ }
461
+ const rotationRad = 0;
462
+ imageLayer.transform = {
463
+ x: position.x,
464
+ y: position.y,
465
+ scaleX: 1,
466
+ scaleY: 1,
467
+ rotation: rotationRad,
468
+ anchorX: 0.5,
469
+ anchorY: 0.5
470
+ };
471
+ }
472
+ return imageLayer;
473
+ }
474
+ return baseLayer;
543
475
  }
544
476
  }
545
477
  export {