Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions src/plugins/crossfade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ export default createPlugin<
onPlayerApiReady() {
let transitionAudio: Howl; // Howler audio used to fade out the current music
let firstVideo = true;
let waitForTransition: Promise<unknown>;
let waitForTransition: Promise<unknown> = Promise.resolve();
let originalVolume: number = 1;

const getStreamURL = async (videoID: string): Promise<string> =>
this.ipc?.invoke('audio-url', videoID) as Promise<string>;
Expand All @@ -199,6 +200,13 @@ export default createPlugin<
const isReadyToCrossfade = () =>
transitionAudio && transitionAudio.state() === 'loaded';

const ensureVideoVolume = () => {
const video = document.querySelector('video');
if (video && video.volume === 0 && originalVolume > 0) {
video.volume = originalVolume;
}
};
Comment on lines +203 to +208
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces non-trivial volume state recovery behavior (tracking/restoring previous volume across navigations). There are Playwright tests in the repo (including plugin-level unit tests), but this plugin has no coverage for crossfade/volume handling. Adding a regression test (unit test for extracted helper or an e2e test that reproduces the “volume stuck at 0 after transition” case) would help prevent future regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +208
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureVideoVolume will restore the video volume to originalVolume whenever video.volume === 0. Since originalVolume defaults to 1 and is updated whenever the user volume is > 0, this can unintentionally unmute users who explicitly set volume to 0 (or start muted), and can also override intentional “mute by volume=0” behavior. Consider tracking whether the plugin set the volume to 0 (e.g., a pluginMuted/didFadeToZero flag) and only restoring in that case, and initialize originalVolume from the current video element when available instead of a hard-coded 1.

Copilot uses AI. Check for mistakes.

const watchVideoIDChanges = (cb: (id: string) => void) => {
window.navigation.addEventListener('navigate', (event) => {
const currentVideoID = getVideoIDFromURL(
Expand All @@ -216,6 +224,7 @@ export default createPlugin<
cb(nextVideoID);
});
} else {
ensureVideoVolume();
cb(nextVideoID);
firstVideo = false;
}
Expand All @@ -239,6 +248,10 @@ export default createPlugin<
const syncVideoWithTransitionAudio = () => {
const video = document.querySelector('video')!;

if (video.volume > 0) {
originalVolume = video.volume;
}

const videoFader = new VolumeFader(video, {
fadeScaling: this.config?.fadeScaling,
fadeDuration: this.config?.fadeInDuration,
Expand All @@ -247,29 +260,40 @@ export default createPlugin<
transitionAudio.play();
transitionAudio.seek(video.currentTime);

video.addEventListener('seeking', () => {
const onSeeking = () => {
transitionAudio.seek(video.currentTime);
});
};

video.addEventListener('pause', () => {
const onPause = () => {
transitionAudio.pause();
});
};

video.addEventListener('play', () => {
const onPlay = () => {
transitionAudio.play();
transitionAudio.seek(video.currentTime);

// Fade in
const videoVolume = video.volume;
const videoVolume = originalVolume || video.volume || 1;
video.volume = 0;
videoFader.fadeTo(videoVolume);
Comment on lines 275 to 278
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The volume restoration/fade target uses originalVolume || video.volume || 1. Because 0 is falsy, a legitimate 0 volume will fall through to 1, which can cause an unwanted jump/unmute. Prefer an explicit > 0 check or nullish coalescing (??) with a separately tracked “last non-zero volume”.

Copilot uses AI. Check for mistakes.
});
};

video.addEventListener('seeking', onSeeking);
video.addEventListener('pause', onPause);
video.addEventListener('play', onPlay);
Comment on lines +281 to +283
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syncVideoWithTransitionAudio adds seeking/pause/play listeners every time it’s called, but they are never removed. Since createAudioForCrossfade calls this on each navigation, listeners can accumulate on the same <video> element, leading to duplicated transitionAudio.play/pause/seek calls and repeated fades. Consider removing prior listeners (store handler refs in outer scope) or using an AbortController/signal to clean them up when unloading/creating a new transitionAudio.

Copilot uses AI. Check for mistakes.

if (!video.paused) {
const videoVolume = originalVolume || video.volume || 1;
if (video.volume === 0) {
videoFader.fadeTo(videoVolume);
}
}

// Exit just before the end for the transition
const transitionBeforeEnd = () => {
if (
video.currentTime >=
video.duration - (this.config?.secondsBeforeEnd ?? 0) &&
video.duration - (this.config?.secondsBeforeEnd ?? 0) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Insert ··

Suggested change
video.duration - (this.config?.secondsBeforeEnd ?? 0) &&
video.duration - (this.config?.secondsBeforeEnd ?? 0) &&

isReadyToCrossfade()
) {
video.removeEventListener('timeupdate', transitionBeforeEnd);
Expand All @@ -284,6 +308,7 @@ export default createPlugin<

const crossfade = (cb: () => void) => {
if (!isReadyToCrossfade()) {
ensureVideoVolume()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Insert ;

Suggested change
ensureVideoVolume()
ensureVideoVolume();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/semi> reported by reviewdog 🐶
Missing semicolon.

Suggested change
ensureVideoVolume()
ensureVideoVolume();

cb();
Comment on lines 310 to 312
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after ensureVideoVolume(); this file otherwise uses semicolons consistently and this will likely fail lint/format checks.

Copilot uses AI. Check for mistakes.
return;
}
Expand All @@ -295,8 +320,12 @@ export default createPlugin<

const video = document.querySelector('video')!;

if (video.volume > 0) {
originalVolume = video.volume;
}

const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
initialVolume: video.volume,
initialVolume: originalVolume,
fadeScaling: this.config?.fadeScaling,
fadeDuration: this.config?.fadeOutDuration,
});
Expand All @@ -313,6 +342,7 @@ export default createPlugin<
await waitForTransition;
const url = await getStreamURL(videoID);
if (!url) {
ensureVideoVolume();
return;
}

Expand Down
Loading