@meframe/core 0.0.45 → 0.0.46
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.
- package/dist/controllers/PlaybackController.d.ts +0 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +4 -8
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +3 -2
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +0 -3
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +10 -43
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +2 -12
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +0 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +5 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +38 -10
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +7 -19
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +15 -98
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/types.d.ts +0 -2
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/workers/stages/compose/{video-compose.worker.CeT5le4s.js → video-compose.worker.BP7fEC58.js} +1 -7
- package/dist/workers/stages/compose/{video-compose.worker.CeT5le4s.js.map → video-compose.worker.BP7fEC58.js.map} +1 -1
- package/dist/workers/worker-manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IAGzC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IAGzC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,oBAAoB,CAAS;IAGrC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IA4C/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAKlC,IAAI,IAAI,IAAI;YAYE,aAAa;IAgC3B,KAAK,IAAI,IAAI;IAiBb,IAAI,IAAI,IAAI;IAuBN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyFzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAQ7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAOrB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAGD,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAKzD,OAAO,CAAC,YAAY;IAiEpB,OAAO,CAAC,UAAU;IAiClB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAQlB;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IA0ClC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAiCzC,uBAAuB;IAuDrC,OAAO,CAAC,SAAS;IAKjB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CASlB;IAEF,OAAO,CAAC,UAAU,CAiBhB;IAEF,OAAO,CAAC,mBAAmB;CAI5B"}
|
|
@@ -23,8 +23,7 @@ class PlaybackController {
|
|
|
23
23
|
fps = 0;
|
|
24
24
|
audioContext;
|
|
25
25
|
audioSession;
|
|
26
|
-
//
|
|
27
|
-
isBuffering = false;
|
|
26
|
+
// Seek tracking
|
|
28
27
|
currentSeekId = 0;
|
|
29
28
|
wasPlayingBeforeSeek = false;
|
|
30
29
|
// Unified window management for both video and audio
|
|
@@ -104,6 +103,7 @@ class PlaybackController {
|
|
|
104
103
|
this.rafId = null;
|
|
105
104
|
}
|
|
106
105
|
this.audioSession.stopPlayback();
|
|
106
|
+
this.currentSeekId++;
|
|
107
107
|
this.eventBus.emit(MeframeEvent.PlaybackPause);
|
|
108
108
|
}
|
|
109
109
|
stop() {
|
|
@@ -133,7 +133,6 @@ class PlaybackController {
|
|
|
133
133
|
const clamped = this.clampTime(timeUs);
|
|
134
134
|
this.currentTimeUs = clamped;
|
|
135
135
|
this.currentSeekId++;
|
|
136
|
-
this.isBuffering = false;
|
|
137
136
|
this.initWindow(clamped);
|
|
138
137
|
this.state = "seeking";
|
|
139
138
|
const seekId = this.currentSeekId;
|
|
@@ -349,7 +348,7 @@ class PlaybackController {
|
|
|
349
348
|
immediate: this.state === "playing"
|
|
350
349
|
});
|
|
351
350
|
if (!renderState) {
|
|
352
|
-
if (this.state === "playing"
|
|
351
|
+
if (this.state === "playing") {
|
|
353
352
|
await this.handlePlaybackBuffering(timeUs);
|
|
354
353
|
}
|
|
355
354
|
return;
|
|
@@ -365,11 +364,10 @@ class PlaybackController {
|
|
|
365
364
|
}
|
|
366
365
|
}
|
|
367
366
|
async handlePlaybackBuffering(timeUs) {
|
|
368
|
-
if (this.
|
|
367
|
+
if (this.state !== "playing") {
|
|
369
368
|
return;
|
|
370
369
|
}
|
|
371
370
|
const seekId = this.currentSeekId;
|
|
372
|
-
this.isBuffering = true;
|
|
373
371
|
this.state = "buffering";
|
|
374
372
|
this.eventBus.emit(MeframeEvent.PlaybackBuffering);
|
|
375
373
|
this.audioSession.stopPlayback();
|
|
@@ -397,8 +395,6 @@ class PlaybackController {
|
|
|
397
395
|
console.error("[PlaybackController] Buffering error:", error);
|
|
398
396
|
this.state = "paused";
|
|
399
397
|
this.eventBus.emit(MeframeEvent.PlaybackError, error);
|
|
400
|
-
} finally {
|
|
401
|
-
this.isBuffering = false;
|
|
402
398
|
}
|
|
403
399
|
}
|
|
404
400
|
clampTime(timeUs) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.js","sources":["../../src/controllers/PlaybackController.ts"],"sourcesContent":["import type {\n IPlaybackController,\n PlaybackState,\n PlaybackOptions,\n IEventBus,\n PreviewHandle,\n TimeUs,\n} from './types';\nimport { MeframeEvent } from '../event/events';\nimport type { GlobalAudioSession } from '../orchestrator/GlobalAudioSession';\nimport type { Orchestrator } from '../orchestrator';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { VideoComposer } from '../stages/compose/VideoComposer';\nimport { isVideoClip } from '../model/types';\n\n/**\n * Playback controller for preview\n * Internal implementation - not exposed directly to external consumers\n */\nexport class PlaybackController implements IPlaybackController, PreviewHandle {\n private orchestrator: Orchestrator;\n private eventBus: IEventBus;\n private canvas: HTMLCanvasElement | OffscreenCanvas;\n private videoComposer: VideoComposer | null = null;\n\n // Playback state\n currentTimeUs: TimeUs = 0;\n private state: PlaybackState = 'idle';\n private playbackRate = 1.0;\n private volume = 1.0;\n private loop = false;\n\n // Animation loop\n private rafId: number | null = null;\n private startTimeUs: TimeUs = 0; // Playback start position in AudioContext timeline (microseconds)\n\n // Frame tracking\n private frameCount = 0;\n private lastFrameTime = 0;\n private fps = 0;\n private audioContext: AudioContext;\n private audioSession: GlobalAudioSession;\n\n // Buffering state\n private isBuffering = false;\n private currentSeekId = 0;\n private wasPlayingBeforeSeek = false;\n\n // Unified window management for both video and audio\n private windowEnd: TimeUs = 0;\n private readonly WINDOW_DURATION = 3_000_000; // 3s decode window\n private readonly PREHEAT_DISTANCE = 1_000_000; // 1s preheat trigger distance\n private preheatInProgress = false;\n\n constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions) {\n this.orchestrator = orchestrator;\n this.audioSession = orchestrator.audioSession;\n this.eventBus = eventBus;\n this.canvas = options.canvas;\n this.audioContext = new AudioContext();\n\n // Initialize VideoComposer for real-time composition\n // Pass canvas as externalCanvas for direct rendering\n const model = orchestrator.compositionModel;\n\n // FIX: Prioritize model renderConfig over canvas current dimensions\n // The canvas should act as a viewport into the renderConfig resolution\n const width = model?.renderConfig?.width || this.canvas.width || 720;\n const height = model?.renderConfig?.height || this.canvas.height || 1280;\n\n this.videoComposer = new VideoComposer({\n width,\n height,\n fps: model?.fps || 30,\n backgroundColor: model?.renderConfig?.backgroundColor || '#000',\n externalCanvas: this.canvas,\n });\n\n // Set initial time if provided\n if (options.startUs !== undefined) {\n this.currentTimeUs = options.startUs;\n }\n\n if (options.rate !== undefined) {\n this.playbackRate = options.rate;\n }\n\n if (options.loop !== undefined) {\n this.loop = options.loop;\n }\n\n if (options.autoStart) {\n this.play();\n }\n\n this.setupEventListeners();\n }\n\n async renderCover(): Promise<void> {\n await this.renderCurrentFrame(0);\n }\n\n // Playback control\n play(): void {\n if (this.state === 'playing') return;\n\n // If playback has ended, reset audio scheduling states for clean replay\n if (this.state === 'ended') {\n this.audioSession.resetPlaybackStates();\n }\n\n this.wasPlayingBeforeSeek = true; // User wants to play\n this.startPlayback();\n }\n\n private async startPlayback(): Promise<void> {\n const wasIdle = this.state === 'idle';\n const seekId = this.currentSeekId;\n this.state = 'playing';\n\n try {\n // Render first frame (may trigger buffering if cache miss)\n await this.renderCurrentFrame(this.currentTimeUs);\n\n // Check if seek happened during render or playback was stopped/paused\n if (seekId !== this.currentSeekId || this.state !== 'playing') {\n return;\n }\n\n // Initialize windows BEFORE starting audio playback\n this.initWindow(this.currentTimeUs);\n\n await this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n\n this.startTimeUs =\n this.audioContext!.currentTime * 1_000_000 - this.currentTimeUs / this.playbackRate;\n\n this.playbackLoop();\n\n this.eventBus.emit(MeframeEvent.PlaybackPlay);\n } catch (error) {\n console.error('[PlaybackController] Failed to start playback:', error);\n this.state = wasIdle ? 'idle' : 'paused';\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n }\n }\n\n pause(): void {\n this.state = 'paused';\n this.wasPlayingBeforeSeek = false; // User explicitly paused\n\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.audioSession.stopPlayback();\n\n this.eventBus.emit(MeframeEvent.PlaybackPause);\n }\n\n stop(): void {\n this.pause();\n this.currentTimeUs = 0;\n this.state = 'idle';\n this.wasPlayingBeforeSeek = false; // Reset seek state\n this.frameCount = 0; // Reset frame counter\n this.lastFrameTime = 0; // Reset frame timing\n\n // Clear canvas\n const ctx = this.canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (ctx && 'clearRect' in ctx) {\n ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.audioSession.reset();\n this.audioSession.resetPlaybackStates();\n\n this.eventBus.emit(MeframeEvent.PlaybackStop);\n }\n\n async seek(timeUs: TimeUs): Promise<void> {\n const previousState = this.state;\n\n this.orchestrator.cancelActiveDecoding();\n\n // Stop playback without changing wasPlayingBeforeSeek\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.audioSession.stopPlayback();\n\n // Reset audio playback states for all clips\n this.audioSession.resetPlaybackStates();\n\n const clamped = this.clampTime(timeUs);\n this.currentTimeUs = clamped;\n this.currentSeekId++; // Invalidate previous seek operations\n this.isBuffering = false; // Reset buffering flag\n\n // Initialize window at seek position\n this.initWindow(clamped);\n\n this.state = 'seeking';\n\n const seekId = this.currentSeekId;\n\n try {\n // Fast path: Try to render nearest keyframe immediately to avoid black screen\n const keyframeTimeUs = await this.orchestrator.tryRenderKeyframe(clamped);\n if (keyframeTimeUs !== null) {\n // Render keyframe from L1 cache\n const renderState = await this.orchestrator.getRenderState(clamped, {\n immediate: true,\n relativeTimeUs: keyframeTimeUs,\n });\n if (renderState && this.videoComposer) {\n await this.videoComposer.composeFrame({\n timeUs: clamped,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n }\n }\n\n // Check if another seek happened during keyframe render\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n // Ensure audio windows ready for seek position (blocking mode)\n await this.audioSession.ensureAudioForTime(clamped, { immediate: false });\n\n // Force decode entire window even if keyframe is in L1\n // preheat: true skips L1 check and decodes full window (60-120 frames)\n await this.orchestrator.getFrame(clamped, {\n immediate: false,\n preheat: true,\n });\n\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n await this.renderCurrentFrame(clamped);\n\n // Check if another seek happened during render\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n this.eventBus.emit(MeframeEvent.PlaybackSeek, { timeUs: this.currentTimeUs });\n\n if (this.wasPlayingBeforeSeek) {\n await this.startPlayback();\n } else {\n this.state = previousState === 'idle' ? 'idle' : 'paused';\n }\n } catch (error) {\n // Check if this seek is still current\n if (seekId !== this.currentSeekId) {\n return;\n }\n console.error('[PlaybackController] Seek error:', error);\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n this.state = previousState === 'idle' ? 'idle' : 'paused';\n }\n }\n\n // Playback properties\n setRate(rate: number): void {\n // Adjust audio start time to maintain current position\n const currentTimeUs = this.currentTimeUs;\n this.playbackRate = rate;\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000 - currentTimeUs / rate;\n\n this.eventBus.emit(MeframeEvent.PlaybackRateChange, { rate });\n\n this.audioSession.setPlaybackRate(this.playbackRate);\n }\n\n setVolume(volume: number): void {\n this.volume = Math.max(0, Math.min(1, volume));\n this.eventBus.emit(MeframeEvent.PlaybackVolumeChange, { volume: this.volume });\n\n this.audioSession.setVolume(this.volume);\n }\n\n setMute(muted: boolean): void {\n if (muted) {\n this.audioSession.stopPlayback();\n } else if (this.state === 'playing') {\n this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n }\n }\n\n setLoop(loop: boolean): void {\n this.loop = loop;\n }\n\n get duration(): TimeUs {\n const modelDuration = this.orchestrator.compositionModel?.durationUs;\n if (modelDuration !== undefined) {\n return modelDuration;\n }\n\n return 0;\n }\n\n get isPlaying(): boolean {\n return this.state === 'playing';\n }\n\n // Resume is just an alias for play\n resume(): void {\n this.play();\n }\n\n on(event: string, handler: (payload: any) => void): void {\n this.eventBus.on(event as MeframeEvent, handler);\n }\n\n off(event: string, handler: (payload: any) => void): void {\n this.eventBus.off(event as MeframeEvent, handler);\n }\n\n // Private methods\n private playbackLoop(): void {\n // State Check #1: Loop entry (from recursive call)\n // Prevents next frame from starting if state changed during previous frame\n // Captures: pause(), stop(), seek() called during frame processing\n if (this.state !== 'playing') {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n return;\n }\n\n this.rafId = requestAnimationFrame(async () => {\n // State Check #2: RAF callback start (async boundary)\n // Captures state changes during RAF delay (~16ms)\n // Scenario: User pauses/seeks between requestAnimationFrame call and callback execution\n if (this.state !== 'playing') {\n return;\n }\n\n this.updateTime();\n\n // State Check #3: After updateTime (playback end detection)\n // CRITICAL: Prevents ensureAudioForTime/scheduleAudio from polluting cache\n // When playback ends, currentTimeUs is reset to 0 for clean replay\n // Without this check, audio operations would execute with wrong position\n if (this.state !== 'playing') {\n return;\n }\n\n // Ensure audio windows ready (aligned with video architecture)\n // Use immediate mode during playback (non-blocking)\n await this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: true });\n\n // Lookahead audio scheduling (OfflineAudioMixer approach)\n await this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);\n\n await this.renderCurrentFrame(this.currentTimeUs);\n\n // State Check #4: After render (buffering detection)\n // Captures: renderCurrentFrame() triggered buffering (cache miss)\n // Optimization: Early exit to avoid unnecessary FPS calculation and setWindow\n // Not strictly required (next loop's Check #1 would catch it), but improves responsiveness\n if (this.state !== 'playing') {\n return;\n }\n\n // Calculate FPS based on actual frame timing\n const now = performance.now();\n if (this.lastFrameTime > 0) {\n const deltaTime = now - this.lastFrameTime;\n const instantFps = 1000 / deltaTime;\n this.fps = this.fps > 0 ? this.fps * 0.9 + instantFps * 0.1 : instantFps;\n }\n this.lastFrameTime = now;\n\n this.frameCount++;\n\n // Update unified window for video and audio\n this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n\n this.playbackLoop();\n });\n }\n\n private updateTime(): void {\n const elapsedUs =\n (this.audioContext!.currentTime * 1_000_000 - this.startTimeUs) * this.playbackRate;\n this.currentTimeUs = elapsedUs;\n\n // Check if reached end\n if (this.currentTimeUs >= this.duration) {\n if (this.loop) {\n this.currentTimeUs = 0;\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000;\n // Reset audio scheduling states to avoid stale state from previous playback\n this.audioSession.resetPlaybackStates();\n // Reset window to start position\n this.initWindow(0);\n } else {\n this.pause();\n // Set to 0 instead of duration to prevent audio operations in final frame\n // from polluting cache with wrong position data\n this.currentTimeUs = 0;\n this.state = 'ended';\n this.eventBus.emit(MeframeEvent.PlaybackEnded, { timeUs: this.duration });\n }\n }\n\n // Emit time update\n this.eventBus.emit(MeframeEvent.PlaybackTimeUpdate, { timeUs: this.currentTimeUs });\n\n // Check if approaching window edge - trigger async preheat\n this.checkAndPreheatWindow();\n // Note: setWindow is now called in Orchestrator.renderFrame()\n // this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n }\n\n /**\n * Initialize window at given time (called on play/seek)\n * Sets unified window for both video and audio\n */\n private initWindow(timeUs: TimeUs): void {\n this.windowEnd = timeUs + this.WINDOW_DURATION;\n this.preheatInProgress = false; // Reset preheat state\n\n // Set unified window center for both video and audio\n this.orchestrator.cacheManager.setWindow(timeUs);\n }\n\n /**\n * Check if approaching window end and trigger preheat for next window\n *\n * Strategy: Unified sliding window for both video and audio\n * - Current window: [windowStart, windowEnd] (3s duration)\n * - When playback reaches windowEnd - 1s, preheat next window\n * - Next window: [windowEnd, windowEnd + 3s]\n */\n private checkAndPreheatWindow(): void {\n // Skip if already preheating or not playing\n if (this.preheatInProgress || this.state !== 'playing') {\n return;\n }\n\n // Check if approaching window end\n const distanceToWindowEnd = this.windowEnd - this.currentTimeUs;\n if (distanceToWindowEnd < 0) {\n this.initWindow(this.currentTimeUs);\n return;\n }\n\n // Trigger preheat when 1s from window end\n if (distanceToWindowEnd > 0 && distanceToWindowEnd <= this.PREHEAT_DISTANCE) {\n this.preheatNextWindow();\n }\n }\n\n /**\n * Preheat next window by decoding from current playback time\n * Supports cross-clip window preheating for seamless playback\n * Preheats both video and audio in parallel\n */\n async preheatNextWindow(): Promise<void> {\n this.preheatInProgress = true;\n try {\n const windowStart = this.currentTimeUs;\n const windowEnd = windowStart + this.WINDOW_DURATION;\n\n // Get all clips in window range (may span multiple clips)\n const clipsInWindow =\n this.orchestrator.compositionModel?.getClipsInRange(windowStart, windowEnd) ?? [];\n const preheatPromises: Promise<any>[] = [];\n\n // Preheat video for each clip in window\n for (const clip of clipsInWindow) {\n if (!isVideoClip(clip)) continue;\n\n // Calculate clip-relative window boundaries\n const clipWindowStart = Math.max(0, windowStart - clip.startUs);\n const clipWindowEnd = Math.min(clip.durationUs, windowEnd - clip.startUs);\n\n // Skip if window doesn't overlap with this clip\n if (clipWindowStart >= clipWindowEnd) continue;\n\n preheatPromises.push(\n this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)\n );\n }\n\n // Preheat audio (non-blocking mode for background decoding)\n preheatPromises.push(this.audioSession.ensureAudioForTime(windowStart, { immediate: false }));\n\n await Promise.all(preheatPromises);\n\n // Update window bounds after successful preheat\n this.windowEnd = windowEnd;\n } catch (error) {\n // Preheat failures are not critical\n console.warn('[PlaybackController] Preheat failed:', error);\n } finally {\n this.preheatInProgress = false;\n }\n }\n\n async renderCurrentFrame(timeUs: TimeUs): Promise<void> {\n if (!this.videoComposer) {\n console.error('[PlaybackController] VideoComposer not initialized');\n return;\n }\n\n try {\n // Get render state (layers) from orchestrator\n // Use immediate mode when playing to detect buffering\n const renderState = await this.orchestrator.getRenderState(timeUs, {\n immediate: this.state === 'playing',\n });\n\n if (!renderState) {\n // If renderState is null during playback, we hit a cache miss/buffer underrun\n if (this.state === 'playing' && !this.isBuffering) {\n await this.handlePlaybackBuffering(timeUs);\n }\n return;\n }\n\n // Compose directly to canvas\n await this.videoComposer.composeFrame({\n timeUs,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n } catch (error) {\n console.error('Render error:', error);\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n }\n }\n\n private async handlePlaybackBuffering(timeUs: TimeUs): Promise<void> {\n if (this.isBuffering || this.state !== 'playing') {\n return;\n }\n\n const seekId = this.currentSeekId;\n this.isBuffering = true;\n this.state = 'buffering';\n this.eventBus.emit(MeframeEvent.PlaybackBuffering);\n\n // Pause audio immediately to prevent desync\n this.audioSession.stopPlayback();\n\n try {\n // Update window center for buffering position\n this.orchestrator.cacheManager.setWindow(timeUs);\n\n // Force load frame (blocking)\n // This ensures the resource is downloaded and decoded\n await this.orchestrator.getFrame(timeUs, { immediate: false });\n\n // Also ensure audio is ready (blocking mode for buffering)\n await this.audioSession.ensureAudioForTime(timeUs, { immediate: false });\n\n // Check if seek happened during buffering or playback was stopped/paused\n if (seekId !== this.currentSeekId || this.state !== 'buffering') {\n return;\n }\n\n this.state = 'playing';\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000 - timeUs / this.playbackRate;\n\n // Resume audio synced with timeline\n await this.audioSession.startPlayback(timeUs, this.audioContext);\n\n this.eventBus.emit(MeframeEvent.PlaybackPlay);\n\n if (!this.rafId) {\n this.playbackLoop();\n }\n } catch (error) {\n // Ignore WaiterReplacedError (happens during fast seeks)\n if (error instanceof WaiterReplacedError) {\n return;\n }\n // Check if seek happened during error handling\n if (seekId !== this.currentSeekId) {\n return;\n }\n console.error('[PlaybackController] Buffering error:', error);\n this.state = 'paused';\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n } finally {\n this.isBuffering = false;\n }\n }\n\n private clampTime(timeUs: TimeUs): TimeUs {\n return Math.max(0, Math.min(timeUs, this.duration));\n }\n\n // Cleanup\n dispose(): void {\n this.stop();\n this.eventBus.off(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.off(MeframeEvent.ModelSet, this.onModelSet);\n if (this.videoComposer) {\n this.videoComposer.dispose();\n this.videoComposer = null;\n }\n }\n\n private onCacheCover = (): void => {\n // Optimization for Quick Start / Fast First Frame:\n // When the first frame of a resource is decoded (e.g. via side-channel parsing),\n // we immediately render it if we are at the start of the timeline.\n // This significantly improves perceived loading speed for compatible formats (e.g. fragmented MP4 with moov at start).\n // For standard formats or when not at time 0, this might be redundant as normal playback loop handles it.\n if (this.state === 'idle' && this.currentTimeUs === 0) {\n this.renderCurrentFrame(0);\n }\n };\n\n private onModelSet = (): void => {\n if (!this.videoComposer || !this.orchestrator.compositionModel) return;\n\n const model = this.orchestrator.compositionModel;\n // Update VideoComposer configuration with new model dimensions and fps\n this.videoComposer.updateConfig({\n width: model.renderConfig?.width || 720,\n height: model.renderConfig?.height || 1280,\n fps: model.fps || 30,\n backgroundColor: model.renderConfig?.backgroundColor || '#000',\n });\n\n // Preheat audio for first frame (non-blocking)\n this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: false });\n\n // Re-render current frame to reflect dimension changes immediately\n this.renderCurrentFrame(this.currentTimeUs);\n };\n\n private setupEventListeners(): void {\n this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlaybackController.js","sources":["../../src/controllers/PlaybackController.ts"],"sourcesContent":["import type {\n IPlaybackController,\n PlaybackState,\n PlaybackOptions,\n IEventBus,\n PreviewHandle,\n TimeUs,\n} from './types';\nimport { MeframeEvent } from '../event/events';\nimport type { GlobalAudioSession } from '../orchestrator/GlobalAudioSession';\nimport type { Orchestrator } from '../orchestrator';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { VideoComposer } from '../stages/compose/VideoComposer';\nimport { isVideoClip } from '../model/types';\n\n/**\n * Playback controller for preview\n * Internal implementation - not exposed directly to external consumers\n */\nexport class PlaybackController implements IPlaybackController, PreviewHandle {\n private orchestrator: Orchestrator;\n private eventBus: IEventBus;\n private canvas: HTMLCanvasElement | OffscreenCanvas;\n private videoComposer: VideoComposer | null = null;\n\n // Playback state\n currentTimeUs: TimeUs = 0;\n private state: PlaybackState = 'idle';\n private playbackRate = 1.0;\n private volume = 1.0;\n private loop = false;\n\n // Animation loop\n private rafId: number | null = null;\n private startTimeUs: TimeUs = 0; // Playback start position in AudioContext timeline (microseconds)\n\n // Frame tracking\n private frameCount = 0;\n private lastFrameTime = 0;\n private fps = 0;\n private audioContext: AudioContext;\n private audioSession: GlobalAudioSession;\n\n // Seek tracking\n private currentSeekId = 0;\n private wasPlayingBeforeSeek = false;\n\n // Unified window management for both video and audio\n private windowEnd: TimeUs = 0;\n private readonly WINDOW_DURATION = 3_000_000; // 3s decode window\n private readonly PREHEAT_DISTANCE = 1_000_000; // 1s preheat trigger distance\n private preheatInProgress = false;\n\n constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions) {\n this.orchestrator = orchestrator;\n this.audioSession = orchestrator.audioSession;\n this.eventBus = eventBus;\n this.canvas = options.canvas;\n this.audioContext = new AudioContext();\n\n // Initialize VideoComposer for real-time composition\n // Pass canvas as externalCanvas for direct rendering\n const model = orchestrator.compositionModel;\n\n // FIX: Prioritize model renderConfig over canvas current dimensions\n // The canvas should act as a viewport into the renderConfig resolution\n const width = model?.renderConfig?.width || this.canvas.width || 720;\n const height = model?.renderConfig?.height || this.canvas.height || 1280;\n\n this.videoComposer = new VideoComposer({\n width,\n height,\n fps: model?.fps || 30,\n backgroundColor: model?.renderConfig?.backgroundColor || '#000',\n externalCanvas: this.canvas,\n });\n\n // Set initial time if provided\n if (options.startUs !== undefined) {\n this.currentTimeUs = options.startUs;\n }\n\n if (options.rate !== undefined) {\n this.playbackRate = options.rate;\n }\n\n if (options.loop !== undefined) {\n this.loop = options.loop;\n }\n\n if (options.autoStart) {\n this.play();\n }\n\n this.setupEventListeners();\n }\n\n async renderCover(): Promise<void> {\n await this.renderCurrentFrame(0);\n }\n\n // Playback control\n play(): void {\n if (this.state === 'playing') return;\n\n // If playback has ended, reset audio scheduling states for clean replay\n if (this.state === 'ended') {\n this.audioSession.resetPlaybackStates();\n }\n\n this.wasPlayingBeforeSeek = true; // User wants to play\n this.startPlayback();\n }\n\n private async startPlayback(): Promise<void> {\n const wasIdle = this.state === 'idle';\n const seekId = this.currentSeekId;\n this.state = 'playing';\n\n try {\n // Render first frame (may trigger buffering if cache miss)\n await this.renderCurrentFrame(this.currentTimeUs);\n\n // Check if seek happened during render or playback was stopped/paused\n if (seekId !== this.currentSeekId || this.state !== 'playing') {\n return;\n }\n\n // Initialize windows BEFORE starting audio playback\n this.initWindow(this.currentTimeUs);\n\n await this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n\n this.startTimeUs =\n this.audioContext!.currentTime * 1_000_000 - this.currentTimeUs / this.playbackRate;\n\n this.playbackLoop();\n\n this.eventBus.emit(MeframeEvent.PlaybackPlay);\n } catch (error) {\n console.error('[PlaybackController] Failed to start playback:', error);\n this.state = wasIdle ? 'idle' : 'paused';\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n }\n }\n\n pause(): void {\n this.state = 'paused';\n this.wasPlayingBeforeSeek = false; // User explicitly paused\n\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n\n this.audioSession.stopPlayback();\n\n // Cancel any ongoing async operations (buffering, seeking, etc.)\n this.currentSeekId++;\n\n this.eventBus.emit(MeframeEvent.PlaybackPause);\n }\n\n stop(): void {\n this.pause();\n this.currentTimeUs = 0;\n this.state = 'idle';\n this.wasPlayingBeforeSeek = false; // Reset seek state\n this.frameCount = 0; // Reset frame counter\n this.lastFrameTime = 0; // Reset frame timing\n\n // Clear canvas\n const ctx = this.canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (ctx && 'clearRect' in ctx) {\n ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.audioSession.reset();\n this.audioSession.resetPlaybackStates();\n\n this.eventBus.emit(MeframeEvent.PlaybackStop);\n }\n\n async seek(timeUs: TimeUs): Promise<void> {\n const previousState = this.state;\n\n this.orchestrator.cancelActiveDecoding();\n\n // Stop playback without changing wasPlayingBeforeSeek\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.audioSession.stopPlayback();\n\n // Reset audio playback states for all clips\n this.audioSession.resetPlaybackStates();\n\n const clamped = this.clampTime(timeUs);\n this.currentTimeUs = clamped;\n this.currentSeekId++; // Invalidate previous seek operations\n\n // Initialize window at seek position\n this.initWindow(clamped);\n\n this.state = 'seeking';\n\n const seekId = this.currentSeekId;\n\n try {\n // Fast path: Try to render nearest keyframe immediately to avoid black screen\n const keyframeTimeUs = await this.orchestrator.tryRenderKeyframe(clamped);\n if (keyframeTimeUs !== null) {\n // Render keyframe from L1 cache\n const renderState = await this.orchestrator.getRenderState(clamped, {\n immediate: true,\n relativeTimeUs: keyframeTimeUs,\n });\n if (renderState && this.videoComposer) {\n await this.videoComposer.composeFrame({\n timeUs: clamped,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n }\n }\n\n // Check if another seek happened during keyframe render\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n // Ensure audio windows ready for seek position (blocking mode)\n await this.audioSession.ensureAudioForTime(clamped, { immediate: false });\n\n // Force decode entire window even if keyframe is in L1\n // preheat: true skips L1 check and decodes full window (60-120 frames)\n await this.orchestrator.getFrame(clamped, {\n immediate: false,\n preheat: true,\n });\n\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n await this.renderCurrentFrame(clamped);\n\n // Check if another seek happened during render\n if (seekId !== this.currentSeekId) {\n return;\n }\n\n this.eventBus.emit(MeframeEvent.PlaybackSeek, { timeUs: this.currentTimeUs });\n\n if (this.wasPlayingBeforeSeek) {\n await this.startPlayback();\n } else {\n this.state = previousState === 'idle' ? 'idle' : 'paused';\n }\n } catch (error) {\n // Check if this seek is still current\n if (seekId !== this.currentSeekId) {\n return;\n }\n console.error('[PlaybackController] Seek error:', error);\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n this.state = previousState === 'idle' ? 'idle' : 'paused';\n }\n }\n\n // Playback properties\n setRate(rate: number): void {\n // Adjust audio start time to maintain current position\n const currentTimeUs = this.currentTimeUs;\n this.playbackRate = rate;\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000 - currentTimeUs / rate;\n\n this.eventBus.emit(MeframeEvent.PlaybackRateChange, { rate });\n\n this.audioSession.setPlaybackRate(this.playbackRate);\n }\n\n setVolume(volume: number): void {\n this.volume = Math.max(0, Math.min(1, volume));\n this.eventBus.emit(MeframeEvent.PlaybackVolumeChange, { volume: this.volume });\n\n this.audioSession.setVolume(this.volume);\n }\n\n setMute(muted: boolean): void {\n if (muted) {\n this.audioSession.stopPlayback();\n } else if (this.state === 'playing') {\n this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n }\n }\n\n setLoop(loop: boolean): void {\n this.loop = loop;\n }\n\n get duration(): TimeUs {\n const modelDuration = this.orchestrator.compositionModel?.durationUs;\n if (modelDuration !== undefined) {\n return modelDuration;\n }\n\n return 0;\n }\n\n get isPlaying(): boolean {\n return this.state === 'playing';\n }\n\n // Resume is just an alias for play\n resume(): void {\n this.play();\n }\n\n on(event: string, handler: (payload: any) => void): void {\n this.eventBus.on(event as MeframeEvent, handler);\n }\n\n off(event: string, handler: (payload: any) => void): void {\n this.eventBus.off(event as MeframeEvent, handler);\n }\n\n // Private methods\n private playbackLoop(): void {\n // State Check #1: Loop entry (from recursive call)\n // Prevents next frame from starting if state changed during previous frame\n // Captures: pause(), stop(), seek() called during frame processing\n if (this.state !== 'playing') {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n return;\n }\n\n this.rafId = requestAnimationFrame(async () => {\n // State Check #2: RAF callback start (async boundary)\n // Captures state changes during RAF delay (~16ms)\n // Scenario: User pauses/seeks between requestAnimationFrame call and callback execution\n if (this.state !== 'playing') {\n return;\n }\n\n this.updateTime();\n\n // State Check #3: After updateTime (playback end detection)\n // CRITICAL: Prevents ensureAudioForTime/scheduleAudio from polluting cache\n // When playback ends, currentTimeUs is reset to 0 for clean replay\n // Without this check, audio operations would execute with wrong position\n if (this.state !== 'playing') {\n return;\n }\n\n // Ensure audio windows ready (aligned with video architecture)\n // Use immediate mode during playback (non-blocking)\n await this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: true });\n\n // Lookahead audio scheduling (OfflineAudioMixer approach)\n await this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);\n\n await this.renderCurrentFrame(this.currentTimeUs);\n\n // State Check #4: After render (buffering detection)\n // Captures: renderCurrentFrame() triggered buffering (cache miss)\n // Optimization: Early exit to avoid unnecessary FPS calculation and setWindow\n // Not strictly required (next loop's Check #1 would catch it), but improves responsiveness\n if (this.state !== 'playing') {\n return;\n }\n\n // Calculate FPS based on actual frame timing\n const now = performance.now();\n if (this.lastFrameTime > 0) {\n const deltaTime = now - this.lastFrameTime;\n const instantFps = 1000 / deltaTime;\n this.fps = this.fps > 0 ? this.fps * 0.9 + instantFps * 0.1 : instantFps;\n }\n this.lastFrameTime = now;\n\n this.frameCount++;\n\n // Update unified window for video and audio\n this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n\n this.playbackLoop();\n });\n }\n\n private updateTime(): void {\n const elapsedUs =\n (this.audioContext!.currentTime * 1_000_000 - this.startTimeUs) * this.playbackRate;\n this.currentTimeUs = elapsedUs;\n\n // Check if reached end\n if (this.currentTimeUs >= this.duration) {\n if (this.loop) {\n this.currentTimeUs = 0;\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000;\n // Reset audio scheduling states to avoid stale state from previous playback\n this.audioSession.resetPlaybackStates();\n // Reset window to start position\n this.initWindow(0);\n } else {\n this.pause();\n // Set to 0 instead of duration to prevent audio operations in final frame\n // from polluting cache with wrong position data\n this.currentTimeUs = 0;\n this.state = 'ended';\n this.eventBus.emit(MeframeEvent.PlaybackEnded, { timeUs: this.duration });\n }\n }\n\n // Emit time update\n this.eventBus.emit(MeframeEvent.PlaybackTimeUpdate, { timeUs: this.currentTimeUs });\n\n // Check if approaching window edge - trigger async preheat\n this.checkAndPreheatWindow();\n // Note: setWindow is now called in Orchestrator.renderFrame()\n // this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n }\n\n /**\n * Initialize window at given time (called on play/seek)\n * Sets unified window for both video and audio\n */\n private initWindow(timeUs: TimeUs): void {\n this.windowEnd = timeUs + this.WINDOW_DURATION;\n this.preheatInProgress = false; // Reset preheat state\n\n // Set unified window center for both video and audio\n this.orchestrator.cacheManager.setWindow(timeUs);\n }\n\n /**\n * Check if approaching window end and trigger preheat for next window\n *\n * Strategy: Unified sliding window for both video and audio\n * - Current window: [windowStart, windowEnd] (3s duration)\n * - When playback reaches windowEnd - 1s, preheat next window\n * - Next window: [windowEnd, windowEnd + 3s]\n */\n private checkAndPreheatWindow(): void {\n // Skip if already preheating or not playing\n if (this.preheatInProgress || this.state !== 'playing') {\n return;\n }\n\n // Check if approaching window end\n const distanceToWindowEnd = this.windowEnd - this.currentTimeUs;\n if (distanceToWindowEnd < 0) {\n this.initWindow(this.currentTimeUs);\n return;\n }\n\n // Trigger preheat when 1s from window end\n if (distanceToWindowEnd > 0 && distanceToWindowEnd <= this.PREHEAT_DISTANCE) {\n this.preheatNextWindow();\n }\n }\n\n /**\n * Preheat next window by decoding from current playback time\n * Supports cross-clip window preheating for seamless playback\n * Preheats both video and audio in parallel\n */\n async preheatNextWindow(): Promise<void> {\n this.preheatInProgress = true;\n try {\n const windowStart = this.currentTimeUs;\n const windowEnd = windowStart + this.WINDOW_DURATION;\n\n // Get all clips in window range (may span multiple clips)\n const clipsInWindow =\n this.orchestrator.compositionModel?.getClipsInRange(windowStart, windowEnd) ?? [];\n const preheatPromises: Promise<any>[] = [];\n\n // Preheat video for each clip in window\n for (const clip of clipsInWindow) {\n if (!isVideoClip(clip)) continue;\n\n // Calculate clip-relative window boundaries\n const clipWindowStart = Math.max(0, windowStart - clip.startUs);\n const clipWindowEnd = Math.min(clip.durationUs, windowEnd - clip.startUs);\n\n // Skip if window doesn't overlap with this clip\n if (clipWindowStart >= clipWindowEnd) continue;\n\n preheatPromises.push(\n this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)\n );\n }\n\n // Preheat audio (non-blocking mode for background decoding)\n preheatPromises.push(this.audioSession.ensureAudioForTime(windowStart, { immediate: false }));\n\n await Promise.all(preheatPromises);\n\n // Update window bounds after successful preheat\n this.windowEnd = windowEnd;\n } catch (error) {\n // Preheat failures are not critical\n console.warn('[PlaybackController] Preheat failed:', error);\n } finally {\n this.preheatInProgress = false;\n }\n }\n\n async renderCurrentFrame(timeUs: TimeUs): Promise<void> {\n if (!this.videoComposer) {\n console.error('[PlaybackController] VideoComposer not initialized');\n return;\n }\n\n try {\n // Get render state (layers) from orchestrator\n // Use immediate mode when playing to detect buffering\n const renderState = await this.orchestrator.getRenderState(timeUs, {\n immediate: this.state === 'playing',\n });\n\n if (!renderState) {\n // If renderState is null during playback, we hit a cache miss/buffer underrun\n if (this.state === 'playing') {\n await this.handlePlaybackBuffering(timeUs);\n }\n return;\n }\n\n // Compose directly to canvas\n await this.videoComposer.composeFrame({\n timeUs,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n } catch (error) {\n console.error('Render error:', error);\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n }\n }\n\n private async handlePlaybackBuffering(timeUs: TimeUs): Promise<void> {\n // Prevent re-entrance: if not playing, skip\n if (this.state !== 'playing') {\n return;\n }\n\n const seekId = this.currentSeekId;\n this.state = 'buffering';\n this.eventBus.emit(MeframeEvent.PlaybackBuffering);\n\n // Pause audio immediately to prevent desync\n this.audioSession.stopPlayback();\n\n try {\n // Update window center for buffering position\n this.orchestrator.cacheManager.setWindow(timeUs);\n\n // Force load frame (blocking)\n // This ensures the resource is downloaded and decoded\n await this.orchestrator.getFrame(timeUs, { immediate: false });\n\n // Also ensure audio is ready (blocking mode for buffering)\n await this.audioSession.ensureAudioForTime(timeUs, { immediate: false });\n\n // Check if seek happened during buffering or playback was stopped/paused\n if (seekId !== this.currentSeekId || this.state !== 'buffering') {\n return;\n }\n\n this.state = 'playing';\n this.startTimeUs = this.audioContext!.currentTime * 1_000_000 - timeUs / this.playbackRate;\n\n // Resume audio synced with timeline\n await this.audioSession.startPlayback(timeUs, this.audioContext);\n\n this.eventBus.emit(MeframeEvent.PlaybackPlay);\n\n if (!this.rafId) {\n this.playbackLoop();\n }\n } catch (error) {\n // Ignore WaiterReplacedError (happens during fast seeks)\n if (error instanceof WaiterReplacedError) {\n return;\n }\n // Check if seek happened during error handling\n if (seekId !== this.currentSeekId) {\n return;\n }\n console.error('[PlaybackController] Buffering error:', error);\n this.state = 'paused';\n this.eventBus.emit(MeframeEvent.PlaybackError, error as Error);\n }\n }\n\n private clampTime(timeUs: TimeUs): TimeUs {\n return Math.max(0, Math.min(timeUs, this.duration));\n }\n\n // Cleanup\n dispose(): void {\n this.stop();\n this.eventBus.off(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.off(MeframeEvent.ModelSet, this.onModelSet);\n if (this.videoComposer) {\n this.videoComposer.dispose();\n this.videoComposer = null;\n }\n }\n\n private onCacheCover = (): void => {\n // Optimization for Quick Start / Fast First Frame:\n // When the first frame of a resource is decoded (e.g. via side-channel parsing),\n // we immediately render it if we are at the start of the timeline.\n // This significantly improves perceived loading speed for compatible formats (e.g. fragmented MP4 with moov at start).\n // For standard formats or when not at time 0, this might be redundant as normal playback loop handles it.\n if (this.state === 'idle' && this.currentTimeUs === 0) {\n this.renderCurrentFrame(0);\n }\n };\n\n private onModelSet = (): void => {\n if (!this.videoComposer || !this.orchestrator.compositionModel) return;\n\n const model = this.orchestrator.compositionModel;\n // Update VideoComposer configuration with new model dimensions and fps\n this.videoComposer.updateConfig({\n width: model.renderConfig?.width || 720,\n height: model.renderConfig?.height || 1280,\n fps: model.fps || 30,\n backgroundColor: model.renderConfig?.backgroundColor || '#000',\n });\n\n // Preheat audio for first frame (non-blocking)\n this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: false });\n\n // Re-render current frame to reflect dimension changes immediately\n this.renderCurrentFrame(this.currentTimeUs);\n };\n\n private setupEventListeners(): void {\n this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompositionModel.d.ts","sourceRoot":"","sources":["../../src/model/CompositionModel.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"CompositionModel.d.ts","sourceRoot":"","sources":["../../src/model/CompositionModel.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,MAAM,EAGP,MAAM,SAAS,CAAC;AAIjB,qBAAa,gBAAgB;IAC3B,SAAgB,OAAO,EAAG,KAAK,CAAU;IACzC,SAAgB,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IAChC,UAAU,EAAG,MAAM,CAAC;IAC3B,SAAgB,WAAW,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,KAAK,EAAE,CAAC;IACvB,SAAgB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAEjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IAEvD,SAAgB,YAAY,CAAC,EAAE;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IAEF,SAAgB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAElC,IAAI,EAAE,oBAAoB;IAwBtC,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAInC,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,GAAG,KAAK,EAAE;IAKhF,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIjC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE;IAoBxD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE;IAqBtD;;;;;;OAMG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE;IAczE,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAKpD,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAcpE;;;OAGG;IACH,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAsCvF;;;;;;OAMG;IACH,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,SAAY,GAAG,GAAG,CAAC,MAAM,CAAC;IAiB3E,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAIxC,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,IAAI;IAOvF,kBAAkB,IAAI,QAAQ,EAAE;IAahC,WAAW,IAAI,MAAM;IAIrB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IASzC,YAAY,CAAC,OAAO,CAAC,EAAE;QACrB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,IAAI,CAAC;QACZ,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;KACzC,GAAG,IAAI;IAiCR,OAAO,CAAC,kBAAkB;IA0D1B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,qBAAqB;IAc7B;;OAEG;IACH,uBAAuB,CACrB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,IAAI;IA2BP;;OAEG;IACH,mBAAmB,IAAI,IAAI;IAY3B,OAAO,CAAC,oBAAoB;IAkF5B,OAAO,CAAC,cAAc;CAoBvB"}
|
|
@@ -369,13 +369,14 @@ ${errors.map((e) => `${e.path}: ${e.message}`).join("\n")}`
|
|
|
369
369
|
if ("text" in attachmentClip && attachmentClip.text) {
|
|
370
370
|
attachmentData.text = attachmentClip.text;
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
const newAttachment = {
|
|
373
373
|
id: `${attachmentKind}-${attachmentClip.id}-${mainClip.id}`,
|
|
374
374
|
kind: attachmentKind,
|
|
375
375
|
startUs: overlap.clipRelativeStart,
|
|
376
376
|
durationUs: overlap.duration,
|
|
377
377
|
data: attachmentData
|
|
378
|
-
}
|
|
378
|
+
};
|
|
379
|
+
mainClip.attachments.push(newAttachment);
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
382
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompositionModel.js","sources":["../../src/model/CompositionModel.ts"],"sourcesContent":["import {\n CompositionModelData,\n Track,\n Clip,\n Resource,\n TimeUs,\n AnimationEffect,\n hasResourceId,\n} from './types';\nimport { binarySearchRange, binarySearchOverlapping } from '../utils/binary-search';\nimport { validateCompositionStructure } from './validation';\n\nexport class CompositionModel {\n public readonly version = '1.0' as const;\n public readonly fps: 24 | 25 | 30 | 60;\n public durationUs!: TimeUs; // Assigned in buildIndexes()\n public readonly mainTrackId: string;\n public tracks: Track[];\n public readonly resources: Map<string, Resource>;\n\n private readonly trackMap: Map<string, Track>;\n private readonly clipMap: Map<string, Clip>;\n private readonly resourceRefCount: Map<string, number>;\n\n public readonly renderConfig?: {\n width: number;\n height: number;\n backgroundColor?: string;\n };\n\n public readonly ext?: Record<string, unknown>;\n\n constructor(data: CompositionModelData) {\n const errors = validateCompositionStructure(data);\n if (errors.length > 0) {\n throw new Error(\n `Validation failed:\\n${errors.map((e) => `${e.path}: ${e.message}`).join('\\n')}`\n );\n }\n\n this.fps = data.fps;\n this.mainTrackId = data.mainTrackId ?? 'main';\n this.tracks = data.tracks;\n this.resources = new Map(Object.entries(data.resources));\n this.renderConfig = data.renderConfig;\n this.ext = data.ext;\n\n // Build indexes\n this.trackMap = new Map();\n this.clipMap = new Map();\n this.resourceRefCount = new Map();\n\n this.buildIndexes();\n }\n\n // Track operations\n findTrack(id: string): Track | null {\n return this.trackMap.get(id) || null;\n }\n\n getTracksByKind(kind: 'video' | 'audio' | 'caption' | 'overlay' | 'fx'): Track[] {\n return this.tracks.filter((track) => track.kind === kind);\n }\n\n // Clip operations with binary search optimization\n findClip(id: string): Clip | null {\n return this.clipMap.get(id) || null;\n }\n\n getClipsAtTime(timeUs: TimeUs, trackId?: string): Clip[] {\n const tracks = trackId ? [this.findTrack(trackId)] : this.tracks;\n const clips: Clip[] = [];\n\n for (const track of tracks) {\n if (!track) continue;\n // Use binary search for single point lookup\n const clip = binarySearchRange(track.clips, timeUs, (entry, _index) => ({\n start: entry.startUs,\n end: entry.startUs + entry.durationUs,\n }));\n\n if (clip) {\n clips.push(clip);\n }\n }\n\n return clips;\n }\n\n getActiveClips(startUs: TimeUs, endUs: TimeUs): Clip[] {\n const clips: Clip[] = [];\n\n for (const track of this.tracks) {\n // Use binary search for range overlap\n const overlappingClips = binarySearchOverlapping(\n track.clips,\n startUs,\n endUs,\n (clip, _index) => ({\n start: clip.startUs,\n end: clip.startUs + clip.durationUs,\n })\n );\n\n clips.push(...overlappingClips);\n }\n\n return clips;\n }\n\n /**\n * Get all clips in a specific track that overlap with the given time range\n * Uses binary search for O(log n + k) performance\n * @param startUs - Range start time (inclusive)\n * @param endUs - Range end time (exclusive)\n * @param trackId - Optional track ID to filter (defaults to main track)\n */\n getClipsInRange(startUs: TimeUs, endUs: TimeUs, trackId?: string): Clip[] {\n const targetTrackId = trackId ?? this.mainTrackId;\n const track = this.findTrack(targetTrackId);\n\n if (!track) {\n return [];\n }\n\n return binarySearchOverlapping(track.clips, startUs, endUs, (clip) => ({\n start: clip.startUs,\n end: clip.startUs + clip.durationUs,\n }));\n }\n\n getClipIdsByResourceId(resourceId: string): string[] {\n const resource = this.resources.get(resourceId);\n return resource?.clipIds || [];\n }\n\n getClipIdAtTime(trackId: string, timeUs: TimeUs): string | undefined {\n const track = this.findTrack(trackId);\n if (!track) {\n return undefined;\n }\n\n const clip = binarySearchRange(track.clips, timeUs, (entry, _index) => ({\n start: entry.startUs,\n end: entry.startUs + entry.durationUs,\n }));\n\n return clip?.id;\n }\n\n /**\n * Get neighboring clips (Prev/Current/Next) at a specific time for video tracks\n * Returns prev, current, and next clip IDs\n */\n getNeighboringClips(timeUs: TimeUs): { prev?: string; current?: string; next?: string } {\n const videoTracks = this.getTracksByKind('video');\n const result: { prev?: string; current?: string; next?: string } = {};\n\n for (const track of videoTracks) {\n const clips = track.clips;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n if (!clip) continue;\n\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (clip.startUs <= timeUs && timeUs < clipEndUs) {\n if (!result.current) {\n result.current = clip.id;\n }\n\n if (i > 0 && !result.prev) {\n const prevClip = clips[i - 1];\n if (prevClip) {\n result.prev = prevClip.id;\n }\n }\n\n if (i < clips.length - 1 && !result.next) {\n const nextClip = clips[i + 1];\n if (nextClip) {\n result.next = nextClip.id;\n }\n }\n }\n }\n }\n\n return result;\n }\n\n /**\n * Get all clip IDs that should be cached using adaptive strategy\n * - Short clips (≤ maxDuration): cache Current + Next (smooth transitions)\n * - Long clips (> maxDuration): cache Current only (memory control)\n * @param timeUs - Current playback time\n * @param maxDuration - Max duration for 2-clip strategy (default 5s)\n */\n getClipsToCacheAtTime(timeUs: TimeUs, maxDuration = 5_000_000): Set<string> {\n const { current, next } = this.getNeighboringClips(timeUs);\n const clipIds = new Set<string>();\n\n if (!current) return clipIds;\n clipIds.add(current);\n\n // Only cache next clip if current clip is short enough\n const currentClip = this.findClip(current);\n if (currentClip && currentClip.durationUs <= maxDuration && next) {\n clipIds.add(next);\n }\n\n return clipIds;\n }\n\n // Resource operations\n getResource(id: string): Resource | null {\n return this.resources.get(id) || null;\n }\n\n updateResourceState(id: string, state: 'pending' | 'loading' | 'ready' | 'error'): void {\n const resource = this.resources.get(id);\n if (resource) {\n resource.state = state;\n }\n }\n\n getUnusedResources(): Resource[] {\n const unused: Resource[] = [];\n\n for (const [id, resource] of this.resources) {\n if (!this.resourceRefCount.has(id) || this.resourceRefCount.get(id) === 0) {\n unused.push(resource);\n }\n }\n\n return unused;\n }\n\n // Time operations\n getDuration(): TimeUs {\n return this.durationUs;\n }\n\n getTrackDuration(trackId: string): TimeUs {\n const track = this.findTrack(trackId);\n if (!track || track.clips.length === 0) return 0;\n\n // Since clips are sorted, last clip determines duration\n const lastClip = track.clips[track.clips.length - 1];\n return (lastClip?.startUs ?? 0) + (lastClip?.durationUs ?? 0);\n }\n\n buildIndexes(options?: {\n incremental?: boolean;\n trackId?: string;\n clipId?: string;\n clip?: Clip;\n operation?: 'add' | 'update' | 'remove';\n }): void {\n const track = options?.trackId ? this.findTrack(options.trackId) : undefined;\n const isAttachmentTrack = track && track.kind !== 'video' && track.kind !== 'audio';\n // Incremental update for video/audio track clip operations\n if (options?.incremental && !isAttachmentTrack && options.clipId && options.operation) {\n const clip = options.clip ?? this.clipMap.get(options.clipId);\n\n if (options.operation === 'add' && clip) {\n this.addClipToIndexes(clip);\n } else if (options.operation === 'remove' && clip) {\n this.removeClipFromIndexes(clip);\n } else if (options.operation === 'update' && clip) {\n // Handle resource change during update\n if (clip.oldResourceId) {\n this.updateClipResourceIndex(\n options.clipId,\n clip.oldResourceId,\n hasResourceId(clip) ? clip.resourceId : undefined\n );\n }\n }\n\n // Recalculate duration only if affected main track\n if (track?.id === this.mainTrackId) {\n this.recalculateDuration();\n }\n return;\n }\n\n // Full rebuild: needed for attachment tracks or initial load\n this.fullRebuildIndexes();\n }\n\n private fullRebuildIndexes(): void {\n // Clear existing indexes\n this.trackMap.clear();\n this.clipMap.clear();\n this.resourceRefCount.clear();\n\n // Step 1: Sink attachment tracks to main track (preserves original tracks)\n this.sinkAttachmentTracks();\n\n let maxEndUs = 0;\n\n // Step 2: Build all indexes in one pass (track, clip, resource)\n for (const track of this.tracks) {\n this.trackMap.set(track.id, track);\n\n for (const clip of track.clips) {\n (clip as Clip).trackId = track.id;\n (clip as Clip).trackKind = track.kind;\n this.clipMap.set(clip.id, clip);\n\n // Main track resource index (only for clips with resourceId)\n if (hasResourceId(clip)) {\n const resource = this.resources.get(clip.resourceId);\n if (resource) {\n resource.clipIds = [...(resource.clipIds || []), clip.id];\n }\n const count = this.resourceRefCount.get(clip.resourceId) || 0;\n this.resourceRefCount.set(clip.resourceId, count + 1);\n }\n\n // Attachment resource indexes (attachments are already sunk)\n const attachments = clip.attachments ?? [];\n for (const attachment of attachments) {\n const attachmentResourceId = attachment.data?.resourceId;\n if (attachmentResourceId && typeof attachmentResourceId === 'string') {\n const attachmentResource = this.resources.get(attachmentResourceId);\n if (attachmentResource) {\n const clipIds = attachmentResource.clipIds || [];\n if (!clipIds.includes(clip.id)) {\n attachmentResource.clipIds = [...clipIds, clip.id];\n }\n }\n const attachmentCount = this.resourceRefCount.get(attachmentResourceId) || 0;\n this.resourceRefCount.set(attachmentResourceId, attachmentCount + 1);\n }\n }\n\n // Calculate max end time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (clipEndUs > maxEndUs) {\n maxEndUs = clipEndUs;\n }\n }\n }\n\n this.durationUs = maxEndUs;\n }\n\n private addClipToIndexes(clip: Clip): void {\n this.clipMap.set(clip.id, clip);\n\n if (hasResourceId(clip)) {\n const resource = this.resources.get(clip.resourceId);\n if (resource) {\n if (!resource.clipIds) {\n resource.clipIds = [];\n }\n if (!resource.clipIds.includes(clip.id)) {\n resource.clipIds.push(clip.id);\n }\n }\n const count = this.resourceRefCount.get(clip.resourceId) || 0;\n this.resourceRefCount.set(clip.resourceId, count + 1);\n }\n }\n\n private removeClipFromIndexes(clip: Clip): void {\n this.clipMap.delete(clip.id);\n\n const resourceId = clip.oldResourceId || (hasResourceId(clip) ? clip.resourceId : undefined);\n if (resourceId) {\n const resource = this.resources.get(resourceId);\n if (resource?.clipIds) {\n resource.clipIds = resource.clipIds.filter((id) => id !== clip.id);\n }\n const count = this.resourceRefCount.get(resourceId) || 0;\n this.resourceRefCount.set(resourceId, Math.max(0, count - 1));\n }\n }\n\n /**\n * Incrementally update resource index when clip's resourceId changes\n */\n updateClipResourceIndex(\n clipId: string,\n oldResourceId: string | undefined,\n newResourceId: string | undefined\n ): void {\n // Remove from old resource\n if (oldResourceId) {\n const oldResource = this.resources.get(oldResourceId);\n if (oldResource?.clipIds) {\n oldResource.clipIds = oldResource.clipIds.filter((id) => id !== clipId);\n }\n const oldCount = this.resourceRefCount.get(oldResourceId) || 0;\n this.resourceRefCount.set(oldResourceId, Math.max(0, oldCount - 1));\n }\n\n // Add to new resource\n if (newResourceId) {\n const newResource = this.resources.get(newResourceId);\n if (newResource) {\n if (!newResource.clipIds) {\n newResource.clipIds = [];\n }\n if (!newResource.clipIds.includes(clipId)) {\n newResource.clipIds.push(clipId);\n }\n }\n const newCount = this.resourceRefCount.get(newResourceId) || 0;\n this.resourceRefCount.set(newResourceId, newCount + 1);\n }\n }\n\n /**\n * Recalculate total duration based on main track\n */\n recalculateDuration(): void {\n const mainTrack = this.findTrack(this.mainTrackId);\n if (!mainTrack || mainTrack.clips.length === 0) {\n this.durationUs = 0;\n return;\n }\n\n // Since clips are sorted, last clip determines duration\n const lastClip = mainTrack.clips[mainTrack.clips.length - 1];\n this.durationUs = (lastClip?.startUs ?? 0) + (lastClip?.durationUs ?? 0);\n }\n\n private sinkAttachmentTracks(): void {\n let mainTrack: Track | undefined;\n const attachmentTracks = [];\n\n // Sort all tracks\n for (const track of this.tracks) {\n track.clips.sort((a, b) => a.startUs - b.startUs);\n\n if (track.id === this.mainTrackId) {\n mainTrack = track;\n continue;\n }\n // Collect attachment tracks for sinking (caption, overlay, fx)\n // Video and audio tracks are not sunk\n if (track.kind === 'caption' || track.kind === 'overlay' || track.kind === 'fx') {\n attachmentTracks.push(track);\n }\n }\n\n if (!mainTrack) {\n throw new Error('Main track not found');\n }\n\n // Clear existing attachments before sinking (in case of rebuild)\n for (const clip of mainTrack.clips) {\n clip.attachments = [];\n }\n\n for (const attachmentTrack of attachmentTracks) {\n for (const attachmentClip of attachmentTrack.clips) {\n // Use track.kind directly as attachment kind\n const attachmentKind = attachmentTrack.kind;\n\n for (const mainClip of mainTrack.clips) {\n const overlap = this.getTimeOverlap(attachmentClip, mainClip);\n if (!overlap) continue;\n\n if (!mainClip.attachments) {\n mainClip.attachments = [];\n }\n\n // Extract animation effect\n const animationEffect = attachmentClip.effects?.find(\n (e) => e.effectType === 'animation'\n ) as AnimationEffect | undefined;\n\n // Extract target dimensions from metadata\n const targetWidth = attachmentClip.metadata?.targetWidth as number | undefined;\n const targetHeight = attachmentClip.metadata?.targetHeight as number | undefined;\n\n const attachmentData: Record<string, unknown> = {\n animation: animationEffect?.params,\n overlayClipStartUs: attachmentClip.startUs,\n mainClipStartUs: mainClip.startUs,\n ...(targetWidth !== undefined && { targetWidth }),\n ...(targetHeight !== undefined && { targetHeight }),\n };\n\n // Add resourceId if exists (trackKind not yet set, check field directly)\n if ('resourceId' in attachmentClip && attachmentClip.resourceId) {\n attachmentData.resourceId = attachmentClip.resourceId;\n }\n\n // Add text if exists (trackKind not yet set, check field directly)\n if ('text' in attachmentClip && attachmentClip.text) {\n attachmentData.text = attachmentClip.text;\n }\n\n mainClip.attachments.push({\n id: `${attachmentKind}-${attachmentClip.id}-${mainClip.id}`,\n kind: attachmentKind,\n startUs: overlap.clipRelativeStart,\n durationUs: overlap.duration,\n data: attachmentData,\n });\n }\n }\n }\n }\n\n private getTimeOverlap(\n attachmentClip: Clip,\n mainClip: Clip\n ): { clipRelativeStart: number; duration: number } | null {\n const attachmentStart = attachmentClip.startUs;\n const attachmentEnd = attachmentClip.startUs + attachmentClip.durationUs;\n const mainStart = mainClip.startUs;\n const mainEnd = mainClip.startUs + mainClip.durationUs;\n\n if (attachmentEnd <= mainStart || attachmentStart >= mainEnd) {\n return null;\n }\n\n const overlapStart = Math.max(attachmentStart, mainStart);\n const overlapEnd = Math.min(attachmentEnd, mainEnd);\n const duration = overlapEnd - overlapStart;\n const clipRelativeStart = overlapStart - mainStart;\n\n return { clipRelativeStart, duration };\n }\n}\n"],"names":[],"mappings":";;;AAYO,MAAM,iBAAiB;AAAA,EACZ,UAAU;AAAA,EACV;AAAA,EACT;AAAA;AAAA,EACS;AAAA,EACT;AAAA,EACS;AAAA,EAEC;AAAA,EACA;AAAA,EACA;AAAA,EAED;AAAA,EAMA;AAAA,EAEhB,YAAY,MAA4B;AACtC,UAAM,SAAS,6BAA6B,IAAI;AAChD,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI;AAAA,QACR;AAAA,EAAuB,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,MAAA;AAAA,IAElF;AAEA,SAAK,MAAM,KAAK;AAChB,SAAK,cAAc,KAAK,eAAe;AACvC,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,IAAI,IAAI,OAAO,QAAQ,KAAK,SAAS,CAAC;AACvD,SAAK,eAAe,KAAK;AACzB,SAAK,MAAM,KAAK;AAGhB,SAAK,+BAAe,IAAA;AACpB,SAAK,8BAAc,IAAA;AACnB,SAAK,uCAAuB,IAAA;AAE5B,SAAK,aAAA;AAAA,EACP;AAAA;AAAA,EAGA,UAAU,IAA0B;AAClC,WAAO,KAAK,SAAS,IAAI,EAAE,KAAK;AAAA,EAClC;AAAA,EAEA,gBAAgB,MAAiE;AAC/E,WAAO,KAAK,OAAO,OAAO,CAAC,UAAU,MAAM,SAAS,IAAI;AAAA,EAC1D;AAAA;AAAA,EAGA,SAAS,IAAyB;AAChC,WAAO,KAAK,QAAQ,IAAI,EAAE,KAAK;AAAA,EACjC;AAAA,EAEA,eAAe,QAAgB,SAA0B;AACvD,UAAM,SAAS,UAAU,CAAC,KAAK,UAAU,OAAO,CAAC,IAAI,KAAK;AAC1D,UAAM,QAAgB,CAAA;AAEtB,eAAW,SAAS,QAAQ;AAC1B,UAAI,CAAC,MAAO;AAEZ,YAAM,OAAO,kBAAkB,MAAM,OAAO,QAAQ,CAAC,OAAO,YAAY;AAAA,QACtE,OAAO,MAAM;AAAA,QACb,KAAK,MAAM,UAAU,MAAM;AAAA,MAAA,EAC3B;AAEF,UAAI,MAAM;AACR,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,SAAiB,OAAuB;AACrD,UAAM,QAAgB,CAAA;AAEtB,eAAW,SAAS,KAAK,QAAQ;AAE/B,YAAM,mBAAmB;AAAA,QACvB,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,CAAC,MAAM,YAAY;AAAA,UACjB,OAAO,KAAK;AAAA,UACZ,KAAK,KAAK,UAAU,KAAK;AAAA,QAAA;AAAA,MAC3B;AAGF,YAAM,KAAK,GAAG,gBAAgB;AAAA,IAChC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,gBAAgB,SAAiB,OAAe,SAA0B;AACxE,UAAM,gBAAgB,WAAW,KAAK;AACtC,UAAM,QAAQ,KAAK,UAAU,aAAa;AAE1C,QAAI,CAAC,OAAO;AACV,aAAO,CAAA;AAAA,IACT;AAEA,WAAO,wBAAwB,MAAM,OAAO,SAAS,OAAO,CAAC,UAAU;AAAA,MACrE,OAAO,KAAK;AAAA,MACZ,KAAK,KAAK,UAAU,KAAK;AAAA,IAAA,EACzB;AAAA,EACJ;AAAA,EAEA,uBAAuB,YAA8B;AACnD,UAAM,WAAW,KAAK,UAAU,IAAI,UAAU;AAC9C,WAAO,UAAU,WAAW,CAAA;AAAA,EAC9B;AAAA,EAEA,gBAAgB,SAAiB,QAAoC;AACnE,UAAM,QAAQ,KAAK,UAAU,OAAO;AACpC,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,kBAAkB,MAAM,OAAO,QAAQ,CAAC,OAAO,YAAY;AAAA,MACtE,OAAO,MAAM;AAAA,MACb,KAAK,MAAM,UAAU,MAAM;AAAA,IAAA,EAC3B;AAEF,WAAO,MAAM;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAoB,QAAoE;AACtF,UAAM,cAAc,KAAK,gBAAgB,OAAO;AAChD,UAAM,SAA6D,CAAA;AAEnE,eAAW,SAAS,aAAa;AAC/B,YAAM,QAAQ,MAAM;AAEpB,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,MAAM,CAAC;AACpB,YAAI,CAAC,KAAM;AAEX,cAAM,YAAY,KAAK,UAAU,KAAK;AAEtC,YAAI,KAAK,WAAW,UAAU,SAAS,WAAW;AAChD,cAAI,CAAC,OAAO,SAAS;AACnB,mBAAO,UAAU,KAAK;AAAA,UACxB;AAEA,cAAI,IAAI,KAAK,CAAC,OAAO,MAAM;AACzB,kBAAM,WAAW,MAAM,IAAI,CAAC;AAC5B,gBAAI,UAAU;AACZ,qBAAO,OAAO,SAAS;AAAA,YACzB;AAAA,UACF;AAEA,cAAI,IAAI,MAAM,SAAS,KAAK,CAAC,OAAO,MAAM;AACxC,kBAAM,WAAW,MAAM,IAAI,CAAC;AAC5B,gBAAI,UAAU;AACZ,qBAAO,OAAO,SAAS;AAAA,YACzB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,sBAAsB,QAAgB,cAAc,KAAwB;AAC1E,UAAM,EAAE,SAAS,KAAA,IAAS,KAAK,oBAAoB,MAAM;AACzD,UAAM,8BAAc,IAAA;AAEpB,QAAI,CAAC,QAAS,QAAO;AACrB,YAAQ,IAAI,OAAO;AAGnB,UAAM,cAAc,KAAK,SAAS,OAAO;AACzC,QAAI,eAAe,YAAY,cAAc,eAAe,MAAM;AAChE,cAAQ,IAAI,IAAI;AAAA,IAClB;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,YAAY,IAA6B;AACvC,WAAO,KAAK,UAAU,IAAI,EAAE,KAAK;AAAA,EACnC;AAAA,EAEA,oBAAoB,IAAY,OAAwD;AACtF,UAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AACtC,QAAI,UAAU;AACZ,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,qBAAiC;AAC/B,UAAM,SAAqB,CAAA;AAE3B,eAAW,CAAC,IAAI,QAAQ,KAAK,KAAK,WAAW;AAC3C,UAAI,CAAC,KAAK,iBAAiB,IAAI,EAAE,KAAK,KAAK,iBAAiB,IAAI,EAAE,MAAM,GAAG;AACzE,eAAO,KAAK,QAAQ;AAAA,MACtB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,cAAsB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,iBAAiB,SAAyB;AACxC,UAAM,QAAQ,KAAK,UAAU,OAAO;AACpC,QAAI,CAAC,SAAS,MAAM,MAAM,WAAW,EAAG,QAAO;AAG/C,UAAM,WAAW,MAAM,MAAM,MAAM,MAAM,SAAS,CAAC;AACnD,YAAQ,UAAU,WAAW,MAAM,UAAU,cAAc;AAAA,EAC7D;AAAA,EAEA,aAAa,SAMJ;AACP,UAAM,QAAQ,SAAS,UAAU,KAAK,UAAU,QAAQ,OAAO,IAAI;AACnE,UAAM,oBAAoB,SAAS,MAAM,SAAS,WAAW,MAAM,SAAS;AAE5E,QAAI,SAAS,eAAe,CAAC,qBAAqB,QAAQ,UAAU,QAAQ,WAAW;AACrF,YAAM,OAAO,QAAQ,QAAQ,KAAK,QAAQ,IAAI,QAAQ,MAAM;AAE5D,UAAI,QAAQ,cAAc,SAAS,MAAM;AACvC,aAAK,iBAAiB,IAAI;AAAA,MAC5B,WAAW,QAAQ,cAAc,YAAY,MAAM;AACjD,aAAK,sBAAsB,IAAI;AAAA,MACjC,WAAW,QAAQ,cAAc,YAAY,MAAM;AAEjD,YAAI,KAAK,eAAe;AACtB,eAAK;AAAA,YACH,QAAQ;AAAA,YACR,KAAK;AAAA,YACL,cAAc,IAAI,IAAI,KAAK,aAAa;AAAA,UAAA;AAAA,QAE5C;AAAA,MACF;AAGA,UAAI,OAAO,OAAO,KAAK,aAAa;AAClC,aAAK,oBAAA;AAAA,MACP;AACA;AAAA,IACF;AAGA,SAAK,mBAAA;AAAA,EACP;AAAA,EAEQ,qBAA2B;AAEjC,SAAK,SAAS,MAAA;AACd,SAAK,QAAQ,MAAA;AACb,SAAK,iBAAiB,MAAA;AAGtB,SAAK,qBAAA;AAEL,QAAI,WAAW;AAGf,eAAW,SAAS,KAAK,QAAQ;AAC/B,WAAK,SAAS,IAAI,MAAM,IAAI,KAAK;AAEjC,iBAAW,QAAQ,MAAM,OAAO;AAC7B,aAAc,UAAU,MAAM;AAC9B,aAAc,YAAY,MAAM;AACjC,aAAK,QAAQ,IAAI,KAAK,IAAI,IAAI;AAG9B,YAAI,cAAc,IAAI,GAAG;AACvB,gBAAM,WAAW,KAAK,UAAU,IAAI,KAAK,UAAU;AACnD,cAAI,UAAU;AACZ,qBAAS,UAAU,CAAC,GAAI,SAAS,WAAW,CAAA,GAAK,KAAK,EAAE;AAAA,UAC1D;AACA,gBAAM,QAAQ,KAAK,iBAAiB,IAAI,KAAK,UAAU,KAAK;AAC5D,eAAK,iBAAiB,IAAI,KAAK,YAAY,QAAQ,CAAC;AAAA,QACtD;AAGA,cAAM,cAAc,KAAK,eAAe,CAAA;AACxC,mBAAW,cAAc,aAAa;AACpC,gBAAM,uBAAuB,WAAW,MAAM;AAC9C,cAAI,wBAAwB,OAAO,yBAAyB,UAAU;AACpE,kBAAM,qBAAqB,KAAK,UAAU,IAAI,oBAAoB;AAClE,gBAAI,oBAAoB;AACtB,oBAAM,UAAU,mBAAmB,WAAW,CAAA;AAC9C,kBAAI,CAAC,QAAQ,SAAS,KAAK,EAAE,GAAG;AAC9B,mCAAmB,UAAU,CAAC,GAAG,SAAS,KAAK,EAAE;AAAA,cACnD;AAAA,YACF;AACA,kBAAM,kBAAkB,KAAK,iBAAiB,IAAI,oBAAoB,KAAK;AAC3E,iBAAK,iBAAiB,IAAI,sBAAsB,kBAAkB,CAAC;AAAA,UACrE;AAAA,QACF;AAGA,cAAM,YAAY,KAAK,UAAU,KAAK;AACtC,YAAI,YAAY,UAAU;AACxB,qBAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAEA,SAAK,aAAa;AAAA,EACpB;AAAA,EAEQ,iBAAiB,MAAkB;AACzC,SAAK,QAAQ,IAAI,KAAK,IAAI,IAAI;AAE9B,QAAI,cAAc,IAAI,GAAG;AACvB,YAAM,WAAW,KAAK,UAAU,IAAI,KAAK,UAAU;AACnD,UAAI,UAAU;AACZ,YAAI,CAAC,SAAS,SAAS;AACrB,mBAAS,UAAU,CAAA;AAAA,QACrB;AACA,YAAI,CAAC,SAAS,QAAQ,SAAS,KAAK,EAAE,GAAG;AACvC,mBAAS,QAAQ,KAAK,KAAK,EAAE;AAAA,QAC/B;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,iBAAiB,IAAI,KAAK,UAAU,KAAK;AAC5D,WAAK,iBAAiB,IAAI,KAAK,YAAY,QAAQ,CAAC;AAAA,IACtD;AAAA,EACF;AAAA,EAEQ,sBAAsB,MAAkB;AAC9C,SAAK,QAAQ,OAAO,KAAK,EAAE;AAE3B,UAAM,aAAa,KAAK,kBAAkB,cAAc,IAAI,IAAI,KAAK,aAAa;AAClF,QAAI,YAAY;AACd,YAAM,WAAW,KAAK,UAAU,IAAI,UAAU;AAC9C,UAAI,UAAU,SAAS;AACrB,iBAAS,UAAU,SAAS,QAAQ,OAAO,CAAC,OAAO,OAAO,KAAK,EAAE;AAAA,MACnE;AACA,YAAM,QAAQ,KAAK,iBAAiB,IAAI,UAAU,KAAK;AACvD,WAAK,iBAAiB,IAAI,YAAY,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,wBACE,QACA,eACA,eACM;AAEN,QAAI,eAAe;AACjB,YAAM,cAAc,KAAK,UAAU,IAAI,aAAa;AACpD,UAAI,aAAa,SAAS;AACxB,oBAAY,UAAU,YAAY,QAAQ,OAAO,CAAC,OAAO,OAAO,MAAM;AAAA,MACxE;AACA,YAAM,WAAW,KAAK,iBAAiB,IAAI,aAAa,KAAK;AAC7D,WAAK,iBAAiB,IAAI,eAAe,KAAK,IAAI,GAAG,WAAW,CAAC,CAAC;AAAA,IACpE;AAGA,QAAI,eAAe;AACjB,YAAM,cAAc,KAAK,UAAU,IAAI,aAAa;AACpD,UAAI,aAAa;AACf,YAAI,CAAC,YAAY,SAAS;AACxB,sBAAY,UAAU,CAAA;AAAA,QACxB;AACA,YAAI,CAAC,YAAY,QAAQ,SAAS,MAAM,GAAG;AACzC,sBAAY,QAAQ,KAAK,MAAM;AAAA,QACjC;AAAA,MACF;AACA,YAAM,WAAW,KAAK,iBAAiB,IAAI,aAAa,KAAK;AAC7D,WAAK,iBAAiB,IAAI,eAAe,WAAW,CAAC;AAAA,IACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,sBAA4B;AAC1B,UAAM,YAAY,KAAK,UAAU,KAAK,WAAW;AACjD,QAAI,CAAC,aAAa,UAAU,MAAM,WAAW,GAAG;AAC9C,WAAK,aAAa;AAClB;AAAA,IACF;AAGA,UAAM,WAAW,UAAU,MAAM,UAAU,MAAM,SAAS,CAAC;AAC3D,SAAK,cAAc,UAAU,WAAW,MAAM,UAAU,cAAc;AAAA,EACxE;AAAA,EAEQ,uBAA6B;AACnC,QAAI;AACJ,UAAM,mBAAmB,CAAA;AAGzB,eAAW,SAAS,KAAK,QAAQ;AAC/B,YAAM,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AAEhD,UAAI,MAAM,OAAO,KAAK,aAAa;AACjC,oBAAY;AACZ;AAAA,MACF;AAGA,UAAI,MAAM,SAAS,aAAa,MAAM,SAAS,aAAa,MAAM,SAAS,MAAM;AAC/E,yBAAiB,KAAK,KAAK;AAAA,MAC7B;AAAA,IACF;AAEA,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AAGA,eAAW,QAAQ,UAAU,OAAO;AAClC,WAAK,cAAc,CAAA;AAAA,IACrB;AAEA,eAAW,mBAAmB,kBAAkB;AAC9C,iBAAW,kBAAkB,gBAAgB,OAAO;AAElD,cAAM,iBAAiB,gBAAgB;AAEvC,mBAAW,YAAY,UAAU,OAAO;AACtC,gBAAM,UAAU,KAAK,eAAe,gBAAgB,QAAQ;AAC5D,cAAI,CAAC,QAAS;AAEd,cAAI,CAAC,SAAS,aAAa;AACzB,qBAAS,cAAc,CAAA;AAAA,UACzB;AAGA,gBAAM,kBAAkB,eAAe,SAAS;AAAA,YAC9C,CAAC,MAAM,EAAE,eAAe;AAAA,UAAA;AAI1B,gBAAM,cAAc,eAAe,UAAU;AAC7C,gBAAM,eAAe,eAAe,UAAU;AAE9C,gBAAM,iBAA0C;AAAA,YAC9C,WAAW,iBAAiB;AAAA,YAC5B,oBAAoB,eAAe;AAAA,YACnC,iBAAiB,SAAS;AAAA,YAC1B,GAAI,gBAAgB,UAAa,EAAE,YAAA;AAAA,YACnC,GAAI,iBAAiB,UAAa,EAAE,aAAA;AAAA,UAAa;AAInD,cAAI,gBAAgB,kBAAkB,eAAe,YAAY;AAC/D,2BAAe,aAAa,eAAe;AAAA,UAC7C;AAGA,cAAI,UAAU,kBAAkB,eAAe,MAAM;AACnD,2BAAe,OAAO,eAAe;AAAA,UACvC;AAEA,mBAAS,YAAY,KAAK;AAAA,YACxB,IAAI,GAAG,cAAc,IAAI,eAAe,EAAE,IAAI,SAAS,EAAE;AAAA,YACzD,MAAM;AAAA,YACN,SAAS,QAAQ;AAAA,YACjB,YAAY,QAAQ;AAAA,YACpB,MAAM;AAAA,UAAA,CACP;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,eACN,gBACA,UACwD;AACxD,UAAM,kBAAkB,eAAe;AACvC,UAAM,gBAAgB,eAAe,UAAU,eAAe;AAC9D,UAAM,YAAY,SAAS;AAC3B,UAAM,UAAU,SAAS,UAAU,SAAS;AAE5C,QAAI,iBAAiB,aAAa,mBAAmB,SAAS;AAC5D,aAAO;AAAA,IACT;AAEA,UAAM,eAAe,KAAK,IAAI,iBAAiB,SAAS;AACxD,UAAM,aAAa,KAAK,IAAI,eAAe,OAAO;AAClD,UAAM,WAAW,aAAa;AAC9B,UAAM,oBAAoB,eAAe;AAEzC,WAAO,EAAE,mBAAmB,SAAA;AAAA,EAC9B;AACF;"}
|
|
1
|
+
{"version":3,"file":"CompositionModel.js","sources":["../../src/model/CompositionModel.ts"],"sourcesContent":["import {\n CompositionModelData,\n Track,\n Clip,\n Resource,\n TimeUs,\n AnimationEffect,\n hasResourceId,\n} from './types';\nimport { binarySearchRange, binarySearchOverlapping } from '../utils/binary-search';\nimport { validateCompositionStructure } from './validation';\n\nexport class CompositionModel {\n public readonly version = '1.0' as const;\n public readonly fps: 24 | 25 | 30 | 60;\n public durationUs!: TimeUs; // Assigned in buildIndexes()\n public readonly mainTrackId: string;\n public tracks: Track[];\n public readonly resources: Map<string, Resource>;\n\n private readonly trackMap: Map<string, Track>;\n private readonly clipMap: Map<string, Clip>;\n private readonly resourceRefCount: Map<string, number>;\n\n public readonly renderConfig?: {\n width: number;\n height: number;\n backgroundColor?: string;\n };\n\n public readonly ext?: Record<string, unknown>;\n\n constructor(data: CompositionModelData) {\n const errors = validateCompositionStructure(data);\n if (errors.length > 0) {\n throw new Error(\n `Validation failed:\\n${errors.map((e) => `${e.path}: ${e.message}`).join('\\n')}`\n );\n }\n\n this.fps = data.fps;\n this.mainTrackId = data.mainTrackId ?? 'main';\n this.tracks = data.tracks;\n this.resources = new Map(Object.entries(data.resources));\n this.renderConfig = data.renderConfig;\n this.ext = data.ext;\n\n // Build indexes\n this.trackMap = new Map();\n this.clipMap = new Map();\n this.resourceRefCount = new Map();\n\n this.buildIndexes();\n }\n\n // Track operations\n findTrack(id: string): Track | null {\n return this.trackMap.get(id) || null;\n }\n\n getTracksByKind(kind: 'video' | 'audio' | 'caption' | 'overlay' | 'fx'): Track[] {\n return this.tracks.filter((track) => track.kind === kind);\n }\n\n // Clip operations with binary search optimization\n findClip(id: string): Clip | null {\n return this.clipMap.get(id) || null;\n }\n\n getClipsAtTime(timeUs: TimeUs, trackId?: string): Clip[] {\n const tracks = trackId ? [this.findTrack(trackId)] : this.tracks;\n const clips: Clip[] = [];\n\n for (const track of tracks) {\n if (!track) continue;\n // Use binary search for single point lookup\n const clip = binarySearchRange(track.clips, timeUs, (entry, _index) => ({\n start: entry.startUs,\n end: entry.startUs + entry.durationUs,\n }));\n\n if (clip) {\n clips.push(clip);\n }\n }\n\n return clips;\n }\n\n getActiveClips(startUs: TimeUs, endUs: TimeUs): Clip[] {\n const clips: Clip[] = [];\n\n for (const track of this.tracks) {\n // Use binary search for range overlap\n const overlappingClips = binarySearchOverlapping(\n track.clips,\n startUs,\n endUs,\n (clip, _index) => ({\n start: clip.startUs,\n end: clip.startUs + clip.durationUs,\n })\n );\n\n clips.push(...overlappingClips);\n }\n\n return clips;\n }\n\n /**\n * Get all clips in a specific track that overlap with the given time range\n * Uses binary search for O(log n + k) performance\n * @param startUs - Range start time (inclusive)\n * @param endUs - Range end time (exclusive)\n * @param trackId - Optional track ID to filter (defaults to main track)\n */\n getClipsInRange(startUs: TimeUs, endUs: TimeUs, trackId?: string): Clip[] {\n const targetTrackId = trackId ?? this.mainTrackId;\n const track = this.findTrack(targetTrackId);\n\n if (!track) {\n return [];\n }\n\n return binarySearchOverlapping(track.clips, startUs, endUs, (clip) => ({\n start: clip.startUs,\n end: clip.startUs + clip.durationUs,\n }));\n }\n\n getClipIdsByResourceId(resourceId: string): string[] {\n const resource = this.resources.get(resourceId);\n return resource?.clipIds || [];\n }\n\n getClipIdAtTime(trackId: string, timeUs: TimeUs): string | undefined {\n const track = this.findTrack(trackId);\n if (!track) {\n return undefined;\n }\n\n const clip = binarySearchRange(track.clips, timeUs, (entry, _index) => ({\n start: entry.startUs,\n end: entry.startUs + entry.durationUs,\n }));\n\n return clip?.id;\n }\n\n /**\n * Get neighboring clips (Prev/Current/Next) at a specific time for video tracks\n * Returns prev, current, and next clip IDs\n */\n getNeighboringClips(timeUs: TimeUs): { prev?: string; current?: string; next?: string } {\n const videoTracks = this.getTracksByKind('video');\n const result: { prev?: string; current?: string; next?: string } = {};\n\n for (const track of videoTracks) {\n const clips = track.clips;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n if (!clip) continue;\n\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (clip.startUs <= timeUs && timeUs < clipEndUs) {\n if (!result.current) {\n result.current = clip.id;\n }\n\n if (i > 0 && !result.prev) {\n const prevClip = clips[i - 1];\n if (prevClip) {\n result.prev = prevClip.id;\n }\n }\n\n if (i < clips.length - 1 && !result.next) {\n const nextClip = clips[i + 1];\n if (nextClip) {\n result.next = nextClip.id;\n }\n }\n }\n }\n }\n\n return result;\n }\n\n /**\n * Get all clip IDs that should be cached using adaptive strategy\n * - Short clips (≤ maxDuration): cache Current + Next (smooth transitions)\n * - Long clips (> maxDuration): cache Current only (memory control)\n * @param timeUs - Current playback time\n * @param maxDuration - Max duration for 2-clip strategy (default 5s)\n */\n getClipsToCacheAtTime(timeUs: TimeUs, maxDuration = 5_000_000): Set<string> {\n const { current, next } = this.getNeighboringClips(timeUs);\n const clipIds = new Set<string>();\n\n if (!current) return clipIds;\n clipIds.add(current);\n\n // Only cache next clip if current clip is short enough\n const currentClip = this.findClip(current);\n if (currentClip && currentClip.durationUs <= maxDuration && next) {\n clipIds.add(next);\n }\n\n return clipIds;\n }\n\n // Resource operations\n getResource(id: string): Resource | null {\n return this.resources.get(id) || null;\n }\n\n updateResourceState(id: string, state: 'pending' | 'loading' | 'ready' | 'error'): void {\n const resource = this.resources.get(id);\n if (resource) {\n resource.state = state;\n }\n }\n\n getUnusedResources(): Resource[] {\n const unused: Resource[] = [];\n\n for (const [id, resource] of this.resources) {\n if (!this.resourceRefCount.has(id) || this.resourceRefCount.get(id) === 0) {\n unused.push(resource);\n }\n }\n\n return unused;\n }\n\n // Time operations\n getDuration(): TimeUs {\n return this.durationUs;\n }\n\n getTrackDuration(trackId: string): TimeUs {\n const track = this.findTrack(trackId);\n if (!track || track.clips.length === 0) return 0;\n\n // Since clips are sorted, last clip determines duration\n const lastClip = track.clips[track.clips.length - 1];\n return (lastClip?.startUs ?? 0) + (lastClip?.durationUs ?? 0);\n }\n\n buildIndexes(options?: {\n incremental?: boolean;\n trackId?: string;\n clipId?: string;\n clip?: Clip;\n operation?: 'add' | 'update' | 'remove';\n }): void {\n const track = options?.trackId ? this.findTrack(options.trackId) : undefined;\n const isAttachmentTrack = track && track.kind !== 'video' && track.kind !== 'audio';\n // Incremental update for video/audio track clip operations\n if (options?.incremental && !isAttachmentTrack && options.clipId && options.operation) {\n const clip = options.clip ?? this.clipMap.get(options.clipId);\n\n if (options.operation === 'add' && clip) {\n this.addClipToIndexes(clip);\n } else if (options.operation === 'remove' && clip) {\n this.removeClipFromIndexes(clip);\n } else if (options.operation === 'update' && clip) {\n // Handle resource change during update\n if (clip.oldResourceId) {\n this.updateClipResourceIndex(\n options.clipId,\n clip.oldResourceId,\n hasResourceId(clip) ? clip.resourceId : undefined\n );\n }\n }\n\n // Recalculate duration only if affected main track\n if (track?.id === this.mainTrackId) {\n this.recalculateDuration();\n }\n return;\n }\n\n // Full rebuild: needed for attachment tracks or initial load\n this.fullRebuildIndexes();\n }\n\n private fullRebuildIndexes(): void {\n // Clear existing indexes\n this.trackMap.clear();\n this.clipMap.clear();\n this.resourceRefCount.clear();\n\n // Step 1: Sink attachment tracks to main track (preserves original tracks)\n this.sinkAttachmentTracks();\n\n let maxEndUs = 0;\n\n // Step 2: Build all indexes in one pass (track, clip, resource)\n for (const track of this.tracks) {\n this.trackMap.set(track.id, track);\n\n for (const clip of track.clips) {\n (clip as Clip).trackId = track.id;\n (clip as Clip).trackKind = track.kind;\n this.clipMap.set(clip.id, clip);\n\n // Main track resource index (only for clips with resourceId)\n if (hasResourceId(clip)) {\n const resource = this.resources.get(clip.resourceId);\n if (resource) {\n resource.clipIds = [...(resource.clipIds || []), clip.id];\n }\n const count = this.resourceRefCount.get(clip.resourceId) || 0;\n this.resourceRefCount.set(clip.resourceId, count + 1);\n }\n\n // Attachment resource indexes (attachments are already sunk)\n const attachments = clip.attachments ?? [];\n for (const attachment of attachments) {\n const attachmentResourceId = attachment.data?.resourceId;\n if (attachmentResourceId && typeof attachmentResourceId === 'string') {\n const attachmentResource = this.resources.get(attachmentResourceId);\n if (attachmentResource) {\n const clipIds = attachmentResource.clipIds || [];\n if (!clipIds.includes(clip.id)) {\n attachmentResource.clipIds = [...clipIds, clip.id];\n }\n }\n const attachmentCount = this.resourceRefCount.get(attachmentResourceId) || 0;\n this.resourceRefCount.set(attachmentResourceId, attachmentCount + 1);\n }\n }\n\n // Calculate max end time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (clipEndUs > maxEndUs) {\n maxEndUs = clipEndUs;\n }\n }\n }\n\n this.durationUs = maxEndUs;\n }\n\n private addClipToIndexes(clip: Clip): void {\n this.clipMap.set(clip.id, clip);\n\n if (hasResourceId(clip)) {\n const resource = this.resources.get(clip.resourceId);\n if (resource) {\n if (!resource.clipIds) {\n resource.clipIds = [];\n }\n if (!resource.clipIds.includes(clip.id)) {\n resource.clipIds.push(clip.id);\n }\n }\n const count = this.resourceRefCount.get(clip.resourceId) || 0;\n this.resourceRefCount.set(clip.resourceId, count + 1);\n }\n }\n\n private removeClipFromIndexes(clip: Clip): void {\n this.clipMap.delete(clip.id);\n\n const resourceId = clip.oldResourceId || (hasResourceId(clip) ? clip.resourceId : undefined);\n if (resourceId) {\n const resource = this.resources.get(resourceId);\n if (resource?.clipIds) {\n resource.clipIds = resource.clipIds.filter((id) => id !== clip.id);\n }\n const count = this.resourceRefCount.get(resourceId) || 0;\n this.resourceRefCount.set(resourceId, Math.max(0, count - 1));\n }\n }\n\n /**\n * Incrementally update resource index when clip's resourceId changes\n */\n updateClipResourceIndex(\n clipId: string,\n oldResourceId: string | undefined,\n newResourceId: string | undefined\n ): void {\n // Remove from old resource\n if (oldResourceId) {\n const oldResource = this.resources.get(oldResourceId);\n if (oldResource?.clipIds) {\n oldResource.clipIds = oldResource.clipIds.filter((id) => id !== clipId);\n }\n const oldCount = this.resourceRefCount.get(oldResourceId) || 0;\n this.resourceRefCount.set(oldResourceId, Math.max(0, oldCount - 1));\n }\n\n // Add to new resource\n if (newResourceId) {\n const newResource = this.resources.get(newResourceId);\n if (newResource) {\n if (!newResource.clipIds) {\n newResource.clipIds = [];\n }\n if (!newResource.clipIds.includes(clipId)) {\n newResource.clipIds.push(clipId);\n }\n }\n const newCount = this.resourceRefCount.get(newResourceId) || 0;\n this.resourceRefCount.set(newResourceId, newCount + 1);\n }\n }\n\n /**\n * Recalculate total duration based on main track\n */\n recalculateDuration(): void {\n const mainTrack = this.findTrack(this.mainTrackId);\n if (!mainTrack || mainTrack.clips.length === 0) {\n this.durationUs = 0;\n return;\n }\n\n // Since clips are sorted, last clip determines duration\n const lastClip = mainTrack.clips[mainTrack.clips.length - 1];\n this.durationUs = (lastClip?.startUs ?? 0) + (lastClip?.durationUs ?? 0);\n }\n\n private sinkAttachmentTracks(): void {\n let mainTrack: Track | undefined;\n const attachmentTracks = [];\n\n // Sort all tracks\n for (const track of this.tracks) {\n track.clips.sort((a, b) => a.startUs - b.startUs);\n\n if (track.id === this.mainTrackId) {\n mainTrack = track;\n continue;\n }\n // Collect attachment tracks for sinking (caption, overlay, fx)\n // Video and audio tracks are not sunk\n if (track.kind === 'caption' || track.kind === 'overlay' || track.kind === 'fx') {\n attachmentTracks.push(track);\n }\n }\n\n if (!mainTrack) {\n throw new Error('Main track not found');\n }\n\n // Clear existing attachments before sinking (in case of rebuild)\n for (const clip of mainTrack.clips) {\n clip.attachments = [];\n }\n\n for (const attachmentTrack of attachmentTracks) {\n for (const attachmentClip of attachmentTrack.clips) {\n // Use track.kind directly as attachment kind\n const attachmentKind = attachmentTrack.kind;\n\n for (const mainClip of mainTrack.clips) {\n const overlap = this.getTimeOverlap(attachmentClip, mainClip);\n if (!overlap) continue;\n\n if (!mainClip.attachments) {\n mainClip.attachments = [];\n }\n\n // Extract animation effect\n const animationEffect = attachmentClip.effects?.find(\n (e) => e.effectType === 'animation'\n ) as AnimationEffect | undefined;\n\n // Extract target dimensions from metadata\n const targetWidth = attachmentClip.metadata?.targetWidth as number | undefined;\n const targetHeight = attachmentClip.metadata?.targetHeight as number | undefined;\n\n const attachmentData: Record<string, unknown> = {\n animation: animationEffect?.params,\n overlayClipStartUs: attachmentClip.startUs,\n mainClipStartUs: mainClip.startUs,\n ...(targetWidth !== undefined && { targetWidth }),\n ...(targetHeight !== undefined && { targetHeight }),\n };\n\n // Add resourceId if exists (trackKind not yet set, check field directly)\n if ('resourceId' in attachmentClip && attachmentClip.resourceId) {\n attachmentData.resourceId = attachmentClip.resourceId;\n }\n\n // Add text if exists (trackKind not yet set, check field directly)\n if ('text' in attachmentClip && attachmentClip.text) {\n attachmentData.text = attachmentClip.text;\n }\n\n const newAttachment = {\n id: `${attachmentKind}-${attachmentClip.id}-${mainClip.id}`,\n kind: attachmentKind,\n startUs: overlap.clipRelativeStart,\n durationUs: overlap.duration,\n data: attachmentData,\n };\n\n mainClip.attachments.push(newAttachment);\n }\n }\n }\n }\n\n private getTimeOverlap(\n attachmentClip: Clip,\n mainClip: Clip\n ): { clipRelativeStart: number; duration: number } | null {\n const attachmentStart = attachmentClip.startUs;\n const attachmentEnd = attachmentClip.startUs + attachmentClip.durationUs;\n const mainStart = mainClip.startUs;\n const mainEnd = mainClip.startUs + mainClip.durationUs;\n\n if (attachmentEnd <= mainStart || attachmentStart >= mainEnd) {\n return null;\n }\n\n const overlapStart = Math.max(attachmentStart, mainStart);\n const overlapEnd = Math.min(attachmentEnd, mainEnd);\n const duration = overlapEnd - overlapStart;\n const clipRelativeStart = overlapStart - mainStart;\n\n return { clipRelativeStart, duration };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -42,11 +42,8 @@ export declare class CompositionPlanner {
|
|
|
42
42
|
private attachmentToLayerPlan;
|
|
43
43
|
private resolveAttachmentLayerType;
|
|
44
44
|
private buildAttachmentPayload;
|
|
45
|
-
private resolveAttachmentResourceState;
|
|
46
45
|
private registerResourceUsage;
|
|
47
46
|
private getDefaultFontTemplate;
|
|
48
|
-
private getResourceState;
|
|
49
|
-
private computeInstructionStatus;
|
|
50
47
|
private buildTransitionPlans;
|
|
51
48
|
private transitionToPlan;
|
|
52
49
|
private getStringField;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompositionPlanner.d.ts","sourceRoot":"","sources":["../../src/orchestrator/CompositionPlanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,IAAI,
|
|
1
|
+
{"version":3,"file":"CompositionPlanner.d.ts","sourceRoot":"","sources":["../../src/orchestrator/CompositionPlanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,IAAI,EAKL,MAAM,UAAU,CAAC;AAGlB,OAAO,KAAK,EACV,kBAAkB,EAQnB,MAAM,gCAAgC,CAAC;AAGxC,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAcD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,UAAU,QAAQ;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,kBAAkB,CAAC;IACjC,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;IAEzD,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAKvC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAwB1D,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIjC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAsBpD;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,gBAAgB,EAAE;IAoEtF,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,QAAQ;IAqBlE,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,oBAAoB;IAyB5B,OAAO,CAAC,qBAAqB;IAiC7B,OAAO,CAAC,0BAA0B;IAiBlC,OAAO,CAAC,sBAAsB;IAmG9B,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,oBAAoB;IA8B5B,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;CAQzB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isVideoClip
|
|
1
|
+
import { isVideoClip } from "../model/types.js";
|
|
2
2
|
import { getFontConfig } from "../stages/compose/font-system/FontManager.js";
|
|
3
3
|
import "../stages/compose/font-system/font-templates.js";
|
|
4
4
|
const DEFAULT_COMPOSITION_WIDTH = 1280;
|
|
@@ -144,7 +144,6 @@ class CompositionPlanner {
|
|
|
144
144
|
const baseConfig = this.buildBaseConfig(clip);
|
|
145
145
|
const layerResult = this.buildLayerPlans(clip);
|
|
146
146
|
const transitions = this.buildTransitionPlans(clip);
|
|
147
|
-
const status = this.computeInstructionStatus(layerResult.layers);
|
|
148
147
|
return {
|
|
149
148
|
instructions: {
|
|
150
149
|
clipId: clip.id,
|
|
@@ -153,7 +152,7 @@ class CompositionPlanner {
|
|
|
153
152
|
baseConfig,
|
|
154
153
|
layers: layerResult.layers,
|
|
155
154
|
transitions,
|
|
156
|
-
status
|
|
155
|
+
status: layerResult.status
|
|
157
156
|
},
|
|
158
157
|
resources: layerResult.resources
|
|
159
158
|
};
|
|
@@ -190,15 +189,13 @@ class CompositionPlanner {
|
|
|
190
189
|
layers.push(layer);
|
|
191
190
|
}
|
|
192
191
|
}
|
|
193
|
-
|
|
194
|
-
return { layers, status: overallStatus, resources };
|
|
192
|
+
return { layers, status: "ready", resources };
|
|
195
193
|
}
|
|
196
194
|
createBaseVideoLayer(clip, resources) {
|
|
197
195
|
if (!isVideoClip(clip)) {
|
|
198
196
|
throw new Error(`Clip ${clip.id} is not a video clip`);
|
|
199
197
|
}
|
|
200
|
-
const
|
|
201
|
-
const status = resourceState === "ready" ? "ready" : "pending";
|
|
198
|
+
const status = "ready";
|
|
202
199
|
this.registerResourceUsage(clip.resourceId, status, resources);
|
|
203
200
|
return {
|
|
204
201
|
layerId: `${clip.id}-base-video`,
|
|
@@ -224,8 +221,7 @@ class CompositionPlanner {
|
|
|
224
221
|
const endUs = Math.min(clipDuration, startUs + attachment.durationUs);
|
|
225
222
|
const type = this.resolveAttachmentLayerType(attachment);
|
|
226
223
|
const payload = this.buildAttachmentPayload(attachment, type);
|
|
227
|
-
const
|
|
228
|
-
const status = resourceState === "ready" ? "ready" : "pending";
|
|
224
|
+
const status = "ready";
|
|
229
225
|
const resourceId = payload.resourceId;
|
|
230
226
|
if (resourceId && typeof resourceId === "string") {
|
|
231
227
|
this.registerResourceUsage(resourceId, status, resources);
|
|
@@ -332,17 +328,6 @@ class CompositionPlanner {
|
|
|
332
328
|
...basePayload
|
|
333
329
|
};
|
|
334
330
|
}
|
|
335
|
-
resolveAttachmentResourceState(attachment, type) {
|
|
336
|
-
if (type === "text") {
|
|
337
|
-
return "ready";
|
|
338
|
-
}
|
|
339
|
-
const resourceId = this.getStringField(attachment.data, "resourceId");
|
|
340
|
-
if (!resourceId) {
|
|
341
|
-
return "pending";
|
|
342
|
-
}
|
|
343
|
-
const resourceState = this.getResourceState(resourceId);
|
|
344
|
-
return resourceState === "ready" ? "ready" : "pending";
|
|
345
|
-
}
|
|
346
331
|
registerResourceUsage(identifier, status, resources) {
|
|
347
332
|
if (!identifier) {
|
|
348
333
|
return;
|
|
@@ -359,13 +344,6 @@ class CompositionPlanner {
|
|
|
359
344
|
const isLandscape = width > height;
|
|
360
345
|
return isLandscape ? "baseSubtitle16_9" : "baseSubtitle";
|
|
361
346
|
}
|
|
362
|
-
getResourceState(resourceId) {
|
|
363
|
-
const resource = this.model?.getResource(resourceId);
|
|
364
|
-
return resource?.state ?? "pending";
|
|
365
|
-
}
|
|
366
|
-
computeInstructionStatus(layers) {
|
|
367
|
-
return layers.some((layer) => layer.status === "pending") ? "pending" : "ready";
|
|
368
|
-
}
|
|
369
347
|
buildTransitionPlans(clip) {
|
|
370
348
|
const transitions = [];
|
|
371
349
|
const track = clip.trackId ? this.model?.findTrack(clip.trackId) : null;
|
|
@@ -420,22 +398,11 @@ class CompositionPlanner {
|
|
|
420
398
|
return typeof value === "number" ? value : void 0;
|
|
421
399
|
}
|
|
422
400
|
needsPlanRefresh(clip, plan) {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (plan.resources.pending.size === 0) {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
for (const resourceId of plan.resources.pending) {
|
|
434
|
-
if (this.getResourceState(resourceId) === "ready") {
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return false;
|
|
401
|
+
const currentAttachmentCount = (clip.attachments ?? []).length;
|
|
402
|
+
const cachedAttachmentCount = plan.instructions.layers.filter(
|
|
403
|
+
(layer) => layer.payload.attachmentId
|
|
404
|
+
).length;
|
|
405
|
+
return currentAttachmentCount !== cachedAttachmentCount;
|
|
439
406
|
}
|
|
440
407
|
}
|
|
441
408
|
export {
|