From b9d68aea624aa9a2ceeff81508a16a1fcfae5da8 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 9 Jul 2026 15:13:38 +0000 Subject: [PATCH] refactor: extract director playback helpers --- js/ltx_director.js | 210 ++++++++++++++++++++++++--------------------- 1 file changed, 114 insertions(+), 96 deletions(-) diff --git a/js/ltx_director.js b/js/ltx_director.js index 6ad1a06..b4ccab8 100644 --- a/js/ltx_director.js +++ b/js/ltx_director.js @@ -2023,6 +2023,105 @@ class TimelineEditor { } } + _buildPlaybackAudioSources() { + const segmentsToPlay = []; + + if (this.audioTrackEnabled && !this.retakeMode) { + for (const seg of this.timeline.audioSegments || []) { + segmentsToPlay.push({ + type: "audio", + originalSeg: seg, + start: seg.start, + length: seg.length, + trimStart: seg.trimStart || 0, + audioFile: seg.audioFile, + audioB64: seg.audioB64, + _blobUrl: seg._blobUrl, + fileSize: seg.fileSize, + }); + } + } + + const isOverrideAudio = !!(this.node.properties.overrideAudio || this.timeline.overrideAudio); + if (isOverrideAudio && !this.retakeMode) { + for (const seg of this.timeline.motionSegments || []) { + if (!seg.videoFile && !seg._blobUrl) continue; + segmentsToPlay.push({ + type: "motion", + originalSeg: seg, + start: seg.start, + length: seg.length, + trimStart: seg.trimStart || 0, + audioFile: seg.videoFile || seg.fileName, + audioB64: null, + _blobUrl: seg._blobUrl, + fileSize: seg.fileSize, + }); + } + } + + return segmentsToPlay; + } + + _buildPlaybackMockSeg(item) { + return { + audioFile: item.audioFile, + audioB64: item.audioB64, + _blobUrl: item._blobUrl, + fileSize: item.fileSize, + waveformPeaks: item.originalSeg.waveformPeaks, + }; + } + + async _preparePlaybackAudioBuffer(item, playId) { + const mockSeg = this._buildPlaybackMockSeg(item); + await this._getOrExtractAudio(mockSeg); + + if (this._currentPlayId !== playId || !this.isPlaying) { + return { aborted: true, mockSeg, audioBuffer: null }; + } + + if (mockSeg.waveformPeaks && !item.originalSeg.waveformPeaks) { + item.originalSeg.waveformPeaks = mockSeg.waveformPeaks; + this.render(); + } + + if (!this._isAudioDecodingAllowed(mockSeg)) { + return { aborted: true, mockSeg, audioBuffer: null }; + } + + let audioBuffer = item.originalSeg._audioBuffer; + if (!audioBuffer) { + if (!mockSeg.audioFile && !mockSeg._blobUrl && !mockSeg.audioB64) { + return { aborted: true, mockSeg, audioBuffer: null }; + } + audioBuffer = await this._loadAudioBuffer(mockSeg); + item.originalSeg._audioBuffer = audioBuffer; + } + + return { aborted: false, mockSeg, audioBuffer }; + } + + _computePlaybackSchedule(item, currentFrameCalculated, currentPlayTime, frameRate) { + const segStartFrame = item.start; + if (currentFrameCalculated < segStartFrame) { + const waitFrames = segStartFrame - currentFrameCalculated; + const waitTimeSec = waitFrames / frameRate; + return { + startTime: currentPlayTime + waitTimeSec, + fileOffsetSec: item.trimStart / frameRate, + playDurationSec: item.length / frameRate, + }; + } + + const framesToSkip = currentFrameCalculated - segStartFrame; + return { + startTime: currentPlayTime, + fileOffsetSec: (item.trimStart + framesToSkip) / frameRate, + playDurationSec: (item.length - framesToSkip) / frameRate, + }; + } + async _preloadAudioSegment(seg) { if (seg._audioBuffer || seg._decoding) return; if (!seg.audioFile && !seg._blobUrl) return; @@ -11399,92 +11498,21 @@ class TimelineEditor { this.playbackStartFrame = this.currentFrame; this.playbackStartTime = this.audioContext.currentTime; - // Build the list of active segments to play - const segmentsToPlay = []; - - // 1. Standard Audio Segments on the audio track (only if the track is enabled and NOT in retake mode) - if (this.audioTrackEnabled && !this.retakeMode) { - if (this.timeline.audioSegments) { - for (let seg of this.timeline.audioSegments) { - segmentsToPlay.push({ - type: 'audio', - originalSeg: seg, - start: seg.start, - length: seg.length, - trimStart: seg.trimStart || 0, - audioFile: seg.audioFile, - audioB64: seg.audioB64, - _blobUrl: seg._blobUrl, - fileSize: seg.fileSize - }); - } - } - } - - // 2. Motion Video Segments (only if overrideAudio toggle is ON and NOT in retake mode) - const isOverrideAudio = !!(this.node.properties.overrideAudio || this.timeline.overrideAudio); - if (isOverrideAudio && !this.retakeMode) { - if (this.timeline.motionSegments) { - for (let seg of this.timeline.motionSegments) { - if (seg.videoFile || seg._blobUrl) { - segmentsToPlay.push({ - type: 'motion', - originalSeg: seg, - start: seg.start, - length: seg.length, - trimStart: seg.trimStart || 0, - audioFile: seg.videoFile || seg.fileName, - audioB64: null, - _blobUrl: seg._blobUrl, - fileSize: seg.fileSize - }); - } - } - } - } + const segmentsToPlay = this._buildPlaybackAudioSources(); // Decode and schedule all scheduled segments that happen AT or AFTER currentFrame in the background for (let item of segmentsToPlay) { const segStartFrame = item.start; const segEndFrame = item.start + item.length; - if (segEndFrame <= this.currentFrame) continue; - - (async () => { - try { - // Build mock seg object for helper compatibility - const mockSeg = { - audioFile: item.audioFile, - audioB64: item.audioB64, - _blobUrl: item._blobUrl, - fileSize: item.fileSize, - waveformPeaks: item.originalSeg.waveformPeaks - }; - - await this._getOrExtractAudio(mockSeg); - - if (this._currentPlayId !== playId || !this.isPlaying) return; - - if (mockSeg.waveformPeaks && !item.originalSeg.waveformPeaks) { - item.originalSeg.waveformPeaks = mockSeg.waveformPeaks; - this.render(); - } - - if (!this._isAudioDecodingAllowed(mockSeg)) { - return; - } - - // Build audio buffer - let audioBuffer = item.originalSeg._audioBuffer; - if (!audioBuffer) { - if (!mockSeg.audioFile && !mockSeg._blobUrl && !mockSeg.audioB64) { - return; - } - audioBuffer = await this._loadAudioBuffer(mockSeg); - item.originalSeg._audioBuffer = audioBuffer; - } - - if (this._currentPlayId !== playId || !this.isPlaying) return; + if (segEndFrame <= this.currentFrame) continue; + + (async () => { + try { + const { aborted, audioBuffer } = await this._preparePlaybackAudioBuffer(item, playId); + if (aborted) return; + + if (this._currentPlayId !== playId || !this.isPlaying) return; // Determine current playback position dynamically in Web Audio time const currentPlayTime = this.audioContext.currentTime; @@ -11494,22 +11522,12 @@ class TimelineEditor { // If playback has already moved beyond the segment end, skip playing it if (currentFrameCalculated >= segEndFrame) return; - let startTime, fileOffsetSec, playDurationSec; - - if (currentFrameCalculated < segStartFrame) { - // Segment starts in the future relative to current playback position - const waitFrames = segStartFrame - currentFrameCalculated; - const waitTimeSec = waitFrames / frameRate; - startTime = currentPlayTime + waitTimeSec; - fileOffsetSec = item.trimStart / frameRate; - playDurationSec = item.length / frameRate; - } else { - // Segment is already playing. Start immediately, but offset into the audio buffer - startTime = currentPlayTime; - const framesToSkip = currentFrameCalculated - segStartFrame; - fileOffsetSec = (item.trimStart + framesToSkip) / frameRate; - playDurationSec = (item.length - framesToSkip) / frameRate; - } + const { startTime, fileOffsetSec, playDurationSec } = this._computePlaybackSchedule( + item, + currentFrameCalculated, + currentPlayTime, + frameRate, + ); if (playDurationSec <= 0) return;