Close the three seams in the crossfade
Track changes were audible: a gap in the audio, a clipped ending, and the outgoing video briefly restarting. Three separate causes, all in the handover. The pointer race is what restarts the video. finishTransition sends trackEnded and the server only moves currentId once it has processed it, so for one round trip state.currentId still names the track that just faded out — which is still in items. The next tick, at most 250ms later, reads that as "the pointer moved somewhere we are not playing" and hard-cuts back to it from zero; the real broadcast then arrives and cuts forward again, restarting the new track too. The player now advances its own pointer as it sends and reconciles against the broadcast, keeping its version while the server still names the finished track and yielding to anything else, which is someone jumping. The fade outlasted its own track. It was triggered at OVERLAP_SECONDS left but ran for a fixed OVERLAP_SECONDS, and the tick that fires it is already up to a quarter second late — getDuration() also reports a length that runs a little past the last of the audio. So the outgoing track always ran out mid-fade: the ending is clipped, and YouTube puts up its end screen, which shows the video's first frame and reads as the track starting over. The fade now runs for what is actually left, less END_GUARD_SECONDS, so it lands before the track does. The gap is the incoming deck buffering on the clock. beginCrossfade loaded the next video and started a wall-clock ramp against it, so the outgoing track was ducked while the incoming one was still fetching its first bytes. The next track is now prerolled PREROLL_SECONDS ahead — loaded, played until YouTube reports it running, then seeked back to 0 and paused, which is what keeps the fade opening on the first bar rather than four seconds in — and the ramp does not open until the incoming deck is genuinely playing. START_WAIT_SECONDS caps that wait so a video that never loads cannot strand a track that is ending. Not verified against live playback: the browser the automation drives renders its tab offscreen, permanently visibilityState "hidden", and Chrome will not start a YouTube video there — the deck loads and reports its duration, but the playhead never leaves zero. Confirmed only that the page loads and the decks build. __player.state gains playhead, preroll and handover, and seekToEnd() now lands before the preroll window so one call exercises the whole handover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vCaS2whbFjExShoPNnS3A
This commit is contained in:
@@ -13,6 +13,25 @@ export const OVERLAP_SECONDS = 5;
|
||||
// How fast the player switches when the user jumps around manually.
|
||||
export const HARD_CUT_SECONDS = 0.4;
|
||||
|
||||
// How long before the crossfade the next video is loaded and buffered on the
|
||||
// idle deck. A deck that is still fetching its first bytes plays silence, and
|
||||
// the fade would ramp straight over that silence — this is what buys the
|
||||
// incoming track enough of a head start to be audible the moment it is needed.
|
||||
export const PREROLL_SECONDS = 4;
|
||||
|
||||
// Landing margin for the crossfade, in seconds. getDuration() is the length of
|
||||
// the video, which is not quite the length of the audio: it runs a little past
|
||||
// the last of the sound, and it is rounded. Finishing the fade this far before
|
||||
// the reported end keeps the outgoing track from running out underneath its own
|
||||
// fade — which is heard as a clipped ending and seen as YouTube's end screen
|
||||
// (the video's thumbnail, i.e. its first frame) flashing up mid-transition.
|
||||
export const END_GUARD_SECONDS = 0.35;
|
||||
|
||||
// How long a crossfade waits for the incoming deck to actually start before it
|
||||
// gives up and fades anyway. Only reached when the preroll did not take — a
|
||||
// hard cut, or a video that is slow to load.
|
||||
export const START_WAIT_SECONDS = 1.5;
|
||||
|
||||
// The video frame takes up this fraction of the screen. Keep this below 0.5:
|
||||
// the corners animation puts a frame-sized image in all four corners, which
|
||||
// only stays overlap-free while two frames fit side by side.
|
||||
|
||||
Reference in New Issue
Block a user