Skip to content

Commit 0b2519e

Browse files
committed
fix: add aggressive volume enforcement for delayed YouTube resets
Addresses remaining volume spikes reported during user testing. YouTube's internal player can reset volume up to ~2s after playback starts (via quality switching, internal init, etc.). The previous fix caught the immediate reset but not these delayed ones. Changes: - Extract enforceVolumeNow() helper for consistent 3-way sync - Add loadedmetadata/loadeddata/canplay listeners to catch YouTube's media lifecycle volume resets - Add 3-second enforcement burst (every 200ms) after video element detection to catch any delayed reset regardless of mechanism - Reduce isEnforcingVolume cooldown from 100ms to 50ms - Re-apply duplicate listener guard (Fix C) that was accidentally dropped
1 parent 12155b6 commit 0b2519e

File tree

1 file changed

+39
-60
lines changed

1 file changed

+39
-60
lines changed

Views/macOS/SingletonPlayerWebView+ObserverScript.swift

Lines changed: 39 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,23 @@ extension SingletonPlayerWebView {
1818
// Volume enforcement: track target volume set by Swift
1919
// Don't set a default - only enforce when explicitly set by Swift
2020
// window.__kasetTargetVolume is set by volume init script at document start
21-
let volumeEnforcementTimeout = null; // Debounce volume enforcement
2221
let isEnforcingVolume = false; // Prevent feedback loops
2322
23+
// Reusable 3-way volume enforcement (video element + YouTube APIs)
24+
function enforceVolumeNow() {
25+
const targetVol = window.__kasetTargetVolume;
26+
const v = document.querySelector('video');
27+
if (!v || typeof targetVol !== 'number' || Math.abs(v.volume - targetVol) <= 0.01) return;
28+
isEnforcingVolume = true;
29+
v.volume = targetVol;
30+
const ytVol = Math.round(targetVol * 100);
31+
const p = document.querySelector('ytmusic-player');
32+
if (p && p.playerApi) p.playerApi.setVolume(ytVol);
33+
const mp = document.getElementById('movie_player');
34+
if (mp && mp.setVolume) mp.setVolume(ytVol);
35+
setTimeout(() => { isEnforcingVolume = false; }, 50);
36+
}
37+
2438
function waitForPlayerBar() {
2539
const playerBar = document.querySelector('ytmusic-player-bar');
2640
if (playerBar) {
@@ -39,6 +53,8 @@ extension SingletonPlayerWebView {
3953
setTimeout(attachVideoListeners, 500);
4054
return;
4155
}
56+
if (video.__kasetListenersAttached) return;
57+
video.__kasetListenersAttached = true;
4258
4359
video.addEventListener('play', startPolling);
4460
video.addEventListener('playing', startPolling);
@@ -81,68 +97,33 @@ extension SingletonPlayerWebView {
8197
});
8298
}
8399
84-
// Volume enforcement: listen for external volume changes with debounce
85-
// This catches YouTube's attempts to change volume and reverts to our target
100+
// Volume enforcement: immediately revert external volume changes
101+
// No debounce — the isEnforcingVolume flag prevents feedback loops.
102+
// A debounce allowed YouTube's rapid-fire init events to keep pushing
103+
// enforcement later, leaving wrong volume audible for 1-2 seconds.
86104
video.addEventListener('volumechange', () => {
87-
// Skip if we're currently enforcing (prevents feedback loop)
88105
if (isEnforcingVolume) return;
89-
90-
// Debounce to prevent rapid-fire enforcement
91-
if (volumeEnforcementTimeout) {
92-
clearTimeout(volumeEnforcementTimeout);
93-
}
94-
volumeEnforcementTimeout = setTimeout(() => {
95-
// Also skip if Swift is actively setting volume
96-
if (window.__kasetIsSettingVolume) {
97-
volumeEnforcementTimeout = null;
98-
return;
99-
}
100-
const targetVol = window.__kasetTargetVolume;
101-
const currentVideo = document.querySelector('video');
102-
// Only enforce if target was explicitly set and differs significantly
103-
if (currentVideo && typeof targetVol === 'number' && Math.abs(currentVideo.volume - targetVol) > 0.01) {
104-
isEnforcingVolume = true;
105-
currentVideo.volume = targetVol;
106-
107-
// Also enforce via YouTube's internal APIs
108-
const ytVolume = Math.round(targetVol * 100);
109-
const player = document.querySelector('ytmusic-player');
110-
if (player && player.playerApi) {
111-
player.playerApi.setVolume(ytVolume);
112-
}
113-
const moviePlayer = document.getElementById('movie_player');
114-
if (moviePlayer && moviePlayer.setVolume) {
115-
moviePlayer.setVolume(ytVolume);
116-
}
117-
118-
// Clear flag after a tick to allow next external change to be caught
119-
setTimeout(() => { isEnforcingVolume = false; }, 10);
120-
}
121-
volumeEnforcementTimeout = null;
122-
}, 100);
106+
if (window.__kasetIsSettingVolume) return;
107+
enforceVolumeNow();
123108
});
124109
125-
// CRITICAL: Apply target volume immediately when video element is first detected
126-
// This handles the case where didFinish already set __kasetTargetVolume but
127-
// the video element didn't exist yet. Without this, YouTube creates video at
128-
// 100% and volumechange may never fire (no change from initial state).
129-
const targetVol = window.__kasetTargetVolume;
130-
if (typeof targetVol === 'number') {
131-
isEnforcingVolume = true;
132-
video.volume = targetVol;
133-
134-
// Also set YouTube's internal player API volume
135-
const player = document.querySelector('ytmusic-player');
136-
if (player && player.playerApi) {
137-
player.playerApi.setVolume(Math.round(targetVol * 100));
138-
}
139-
const moviePlayer = document.getElementById('movie_player');
140-
if (moviePlayer && moviePlayer.setVolume) {
141-
moviePlayer.setVolume(Math.round(targetVol * 100));
142-
}
110+
// Enforce volume at media lifecycle events where YouTube resets volume.
111+
// YouTube's player often restores its stored volume at these points.
112+
video.addEventListener('loadedmetadata', () => enforceVolumeNow());
113+
video.addEventListener('loadeddata', () => enforceVolumeNow());
114+
video.addEventListener('canplay', () => enforceVolumeNow());
143115
144-
setTimeout(() => { isEnforcingVolume = false; }, 10);
145-
}
116+
// Apply target volume immediately when video element is first detected
117+
enforceVolumeNow();
118+
119+
// Startup enforcement burst: YouTube may reset volume up to ~2s after
120+
// playback starts (via internal player init, quality switching, etc.).
121+
// Enforce every 200ms for the first 3 seconds to catch delayed resets.
122+
let burstCount = 0;
123+
const burstInterval = setInterval(() => {
124+
enforceVolumeNow();
125+
if (++burstCount >= 15) clearInterval(burstInterval);
126+
}, 200);
146127
147128
// Start polling if already playing
148129
if (!video.paused) {
@@ -155,9 +136,7 @@ extension SingletonPlayerWebView {
155136
const videoObserver = new MutationObserver(() => {
156137
const video = document.querySelector('video');
157138
if (video && !video.__kasetListenersAttached) {
158-
video.__kasetListenersAttached = true;
159139
attachVideoListeners();
160-
// Note: attachVideoListeners now applies target volume immediately
161140
}
162141
});
163142
videoObserver.observe(document.body, { childList: true, subtree: true });

0 commit comments

Comments
 (0)