Fix multi-scene chain preview dying mid-playback in the player.

Export inSec was already correct; the monitor was tearing down its video DOM on
store updates during playback and racing boundary seeks. Keep the pool stable
while playing, guard seek-in-flight, and recover instead of hard-pausing.
This commit is contained in:
digital-garbage
2026-06-11 22:29:53 +03:00
parent 2b7b9c52d8
commit d725a42ae4
+59 -17
View File
@@ -28,6 +28,7 @@
let _pendingFxOffset = 0;
let _seekToken = 0;
let _scrubDepth = 0; // suppress render()-driven re-seek while dragging playhead
let _clipBoundaryToken = 0; // suppress double-advance while a boundary seek is in flight
const _insPool = new Map(); // track id -> <audio>
let _insTracks = [];
@@ -91,7 +92,27 @@
_videoSeekTimer = setTimeout(() => _flushVideoSeek(false), _playing ? 0 : 24);
}
function _segmentSourceBadge(sceneId) {
function _isSeeking() {
if (_seekPending != null || _playPending) return true;
const v = _active;
if (!v) return false;
if (v._pmSeekPending != null) return true;
try { if (v.seeking) return true; } catch (_) {}
return false;
}
function _clipWithinSec() {
if (!_active || !_currentClip || _currentClip.pending) return 0;
return _active.currentTime - (_currentClip.inSec || 0);
}
function _onVideoSeeked(v) {
if (v !== _active || !_currentClip) return;
_clipBoundaryToken = 0;
const within = Math.max(0, _clipWithinSec());
try { _applyFx(_currentClip, within); } catch (_) {}
if (_playing && v.paused) v.play().catch(() => {});
}
if (!sceneId) return "";
const sc = S.scene(sceneId);
if (S.isVideoClip?.(sc)) return "Video clip";
@@ -343,11 +364,11 @@
if (_playPending) { _playPending = false; v.play().catch(() => {}); }
if (_currentClip) _applyFx(_currentClip, Math.max(0, _phSec - _currentClip.startSec));
});
v.addEventListener("seeked", () => {
if (v !== _active || !_currentClip || _playing) return;
_applyFx(_currentClip, Math.max(0, _phSec - _currentClip.startSec));
v.addEventListener("seeked", () => { _onVideoSeeked(v); });
v.addEventListener("ended", () => {
if (v !== _active || _isSeeking() || _clipBoundaryToken) return;
_advance();
});
v.addEventListener("ended", () => { if (v === _active) _advance(); });
stage.append(viewport); pool.set(url, v);
return v;
}
@@ -371,6 +392,7 @@
v._pmFx?.viewport.remove();
}
pool.clear(); _active = null; _currentClip = null; _seekPending = null; _playPending = false;
_clipBoundaryToken = 0;
_pendingSeekSec = null;
_scrubDepth = 0;
if (_seekRaf) { cancelAnimationFrame(_seekRaf); _seekRaf = null; }
@@ -491,15 +513,17 @@
_applyClipUi(clip);
const target = (clip.inSec || 0) + Math.max(0, offset);
if (v.readyState >= 1) {
if (play && Math.abs(v.currentTime - _clampT(v, target)) > 0.04) _clipBoundaryToken++;
_applyVideoTime(v, target, !play);
v._pmSeekPending = null;
_seekPending = null;
if (play) { v.play().catch(() => {}); _startTick(); } else _playPending = false;
if (play) { v.play().catch(() => {}); _startTick(); } else { _playPending = false; _clipBoundaryToken = 0; }
} else {
v._pmSeekPending = target;
_seekPending = target;
_playPending = !!play;
if (play) _startTick();
if (play) { _clipBoundaryToken++; _startTick(); }
else _clipBoundaryToken = 0;
}
_applyFx(clip, Math.max(0, offset));
}
@@ -520,11 +544,18 @@
&& Math.abs((next.inSec || 0) - ((prev.inSec || 0) + prev.durationSec)) < 0.05;
_phSec = next.startSec;
_currentClip = next;
// Reseek on gen-unit / scene boundaries — chain carry scenes overlap in the source file.
const needSeek = !sharedSource || !contiguous
|| (next.sceneId && prev.sceneId && next.sceneId !== prev.sceneId)
|| (next.ghostId && prev.ghostId && next.ghostId !== prev.ghostId);
const crossClip = next.sceneId !== prev.sceneId || next.ghostId !== prev.ghostId;
const needSeek = !sharedSource || !contiguous || crossClip;
if (needSeek) _goto(next, 0, _playing);
else {
_clipBoundaryToken = 0;
_applyClipUi(next);
if (_active) {
const within = Math.max(0, _clipWithinSec());
try { _applyFx(next, within); } catch (_) {}
}
if (_playing) _startTick();
}
_notifyPh();
}
@@ -542,11 +573,20 @@
if (_playing) _raf = requestAnimationFrame(_tick);
return;
}
if (_isSeeking()) {
if (_playing) _raf = requestAnimationFrame(_tick);
return;
}
if (!_active) {
if (_currentClip.media || _currentClip.binUrl) {
_goto(_currentClip, Math.max(0, _phSec - _currentClip.startSec), true);
if (_playing) _raf = requestAnimationFrame(_tick);
return;
}
if (_playing) _pause();
return;
}
const within = _active.currentTime - (_currentClip.inSec || 0);
const within = _clipWithinSec();
if (within >= _currentClip.durationSec - 0.001) { _advance(); }
else { _phSec = _currentClip.startSec + Math.max(0, within); try { _applyFx(_currentClip, within); } catch (_) {} _syncInsAudio(); _notifyPh(); }
if (_playing) _raf = requestAnimationFrame(_tick);
@@ -639,12 +679,15 @@
function _pause() {
if (_active) _active.pause();
const wasPlaying = _playing;
_playing = false;
_playPending = false;
_clipBoundaryToken = 0;
_stopTick();
_pauseInsAudio();
_notifyPh();
_renderTransport();
if (wasPlaying) render(S.get());
}
function _stop() {
@@ -842,12 +885,11 @@
const p = st.project;
const gen = st.gen || { state: "idle", media: [] };
_fpsCur = p?.frame_rate || 25;
_totalSecCur = S.previewTotalSec ? S.previewTotalSec() : 0;
// While scrubbing, keep the program monitor DOM stable (no clear/rebuild).
if (_scrubDepth > 0) {
_totalSecCur = S.previewTotalSec ? S.previewTotalSec() : 0;
return;
}
// Rebuilding the monitor DOM during playback detaches <video> nodes and kills multi-scene
// chain preview mid-stream (often visible from scene 3 onward once store ticks catch up).
if (_scrubDepth > 0 || _playing) return;
clear(body);