@@ -78,7 +78,7 @@ object FingerprintScriptGenerator {
7878 if (window.__feather_bg_play_active) return;
7979 window.__feather_bg_play_active = true;
8080
81- // 1. Override Page Visibility API on both document instance and Document.prototype
81+ // 1. Spoof Page Visibility API so websites (like YouTube) report visible and focused
8282 try {
8383 const defineProp = function(obj, prop, value) {
8484 try {
@@ -102,87 +102,31 @@ object FingerprintScriptGenerator {
102102
103103 document.hasFocus = function() { return true; };
104104 Document.prototype.hasFocus = function() { return true; };
105-
106- // Override property event handlers
107- defineProp(document, 'onvisibilitychange', null);
108- defineProp(window, 'onpagehide', null);
109- defineProp(window, 'onblur', null);
110105 } catch(e) {}
111106
112- // 2. Intercept registration of visibility pause event listeners
113- const origAddEventListener = EventTarget.prototype.addEventListener;
114- EventTarget.prototype.addEventListener = function(type, listener, options) {
115- if (type === 'visibilitychange' || type === 'webkitvisibilitychange' || type === 'pagehide' || type === 'blur') {
116- return;
117- }
118- return origAddEventListener.apply(this, arguments);
119- };
120-
121- // 3. Suppress any bubbling visibilitychange & blur events
122- const suppressEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur', 'freeze'];
123- suppressEvents.forEach(function(evt) {
124- origAddEventListener.call(window, evt, function(e) {
125- if (e) {
126- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
127- if (e.stopPropagation) e.stopPropagation();
128- }
129- }, true);
130- origAddEventListener.call(document, evt, function(e) {
131- if (e) {
132- if (e.stopImmediatePropagation) e.stopImmediatePropagation();
133- if (e.stopPropagation) e.stopPropagation();
107+ // 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange event listeners
108+ try {
109+ const origAddEventListener = EventTarget.prototype.addEventListener;
110+ EventTarget.prototype.addEventListener = function(type, listener, options) {
111+ if (type === 'visibilitychange' || type === 'webkitvisibilitychange') {
112+ return;
134113 }
135- }, true);
136- });
137-
138- // 4. Wrap IntersectionObserver so YouTube / video players never think they are offscreen
139- if (window.IntersectionObserver) {
140- const OrigIO = window.IntersectionObserver;
141- window.IntersectionObserver = function(callback, options) {
142- const wrappedCb = function(entries, observer) {
143- const modEntries = entries.map(function(entry) {
144- return new Proxy(entry, {
145- get: function(target, prop) {
146- if (prop === 'isIntersecting') return true;
147- if (prop === 'intersectionRatio') return 1.0;
148- return target[prop];
149- }
150- });
151- });
152- return callback(modEntries, observer);
153- };
154- return new OrigIO(wrappedCb, options);
114+ return origAddEventListener.apply(this, arguments);
155115 };
156- window.IntersectionObserver.prototype = OrigIO.prototype;
157- }
158116
159- // 5. Track user interaction with player controls vs backgrounding
160- window.__feather_explicit_pause = false;
161- let userInteractedWithPlayer = false;
162- let userPlayerTimer = null;
163-
164- function markPlayerTap() {
165- userInteractedWithPlayer = true;
166- if (userPlayerTimer) clearTimeout(userPlayerTimer);
167- userPlayerTimer = setTimeout(function() {
168- userInteractedWithPlayer = false;
169- }, 1500);
170- }
171-
172- ['click', 'touchend', 'pointerup'].forEach(function(evt) {
173- document.addEventListener(evt, function(e) {
174- try {
175- const target = e.target;
176- if (!target) return;
177- if (target.tagName === 'VIDEO' ||
178- (target.closest && target.closest('video, #movie_player, .html5-video-player, .ytp-play-button, [aria-label*="Pause"], [aria-label*="Play"], .player-controls, ytm-custom-control-button'))) {
179- markPlayerTap();
180- }
181- } catch(err) {}
182- }, true);
183- });
117+ ['visibilitychange', 'webkitvisibilitychange'].forEach(function(evt) {
118+ origAddEventListener.call(window, evt, function(e) {
119+ if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
120+ if (e && e.stopPropagation) e.stopPropagation();
121+ }, true);
122+ origAddEventListener.call(document, evt, function(e) {
123+ if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
124+ if (e && e.stopPropagation) e.stopPropagation();
125+ }, true);
126+ });
127+ } catch(e) {}
184128
185- // 6 . Media Session API hooking for notification media player
129+ // 3 . Media Session Hooking for Android notification metadata & controls
186130 window.__feather_actions = window.__feather_actions || {};
187131
188132 function getMediaThumbnail() {
@@ -283,66 +227,75 @@ object FingerprintScriptGenerator {
283227 }
284228 } catch(e) {}
285229
286- // 7. Direct media element tracking & action triggers
230+ // 4. Media control methods callable from notification bar
287231 window.__feather_media_play = function() {
288- window.__feather_explicit_pause = false;
289- userInteractedWithPlayer = false;
232+ try {
233+ if (window.__feather_actions && typeof window.__feather_actions['play'] === 'function') {
234+ window.__feather_actions['play']();
235+ return;
236+ }
237+ } catch(e) {}
290238 try {
291239 const moviePlayer = document.getElementById('movie_player');
292240 if (moviePlayer && typeof moviePlayer.playVideo === 'function') {
293241 moviePlayer.playVideo();
242+ return;
243+ }
244+ } catch(e) {}
245+ try {
246+ const playBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Play"]');
247+ if (playBtn) {
248+ playBtn.click();
249+ return;
294250 }
295251 } catch(e) {}
296252 try {
297253 const mediaEls = document.querySelectorAll('video, audio');
298254 mediaEls.forEach(function(m) {
299- m.play().catch(function() {});
255+ if (m.paused) m.play().catch(function() {});
300256 });
301257 } catch(e) {}
302- try {
303- if (window.__feather_actions && typeof window.__feather_actions['play'] === 'function') {
304- window.__feather_actions['play']();
305- }
306- } catch(e) {}
307- notifyMediaBridge(true);
308258 };
309259
310260 window.__feather_media_pause = function() {
311- window.__feather_explicit_pause = true;
261+ try {
262+ if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
263+ window.__feather_actions['pause']();
264+ return;
265+ }
266+ } catch(e) {}
312267 try {
313268 const moviePlayer = document.getElementById('movie_player');
314269 if (moviePlayer && typeof moviePlayer.pauseVideo === 'function') {
315270 moviePlayer.pauseVideo();
271+ return;
272+ }
273+ } catch(e) {}
274+ try {
275+ const pauseBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Pause"]');
276+ if (pauseBtn) {
277+ pauseBtn.click();
278+ return;
316279 }
317280 } catch(e) {}
318281 try {
319282 const mediaEls = document.querySelectorAll('video, audio');
320283 mediaEls.forEach(function(m) {
321- if (typeof origPause === 'function') {
322- origPause.call(m);
323- } else {
324- m.pause();
325- }
284+ if (!m.paused) m.pause();
326285 });
327286 } catch(e) {}
328- try {
329- if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
330- window.__feather_actions['pause']();
331- }
332- } catch(e) {}
333- notifyMediaBridge(false);
334287 };
335288
336289 window.__feather_media_toggle = function() {
337- const video = document.querySelector('video');
290+ const video = document.querySelector('video, audio ');
338291 if (video) {
339292 if (video.paused) {
340293 window.__feather_media_play();
341294 } else {
342295 window.__feather_media_pause();
343296 }
344297 } else {
345- const btn = document.querySelector('.ytp-play-button') || document.querySelector(' [aria-label*="Play"], [aria-label*="Pause"]');
298+ const btn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Play"], [aria-label*="Pause"]');
346299 if (btn) btn.click();
347300 }
348301 };
@@ -354,7 +307,7 @@ object FingerprintScriptGenerator {
354307 return;
355308 }
356309 } catch(e) {}
357- const nextBtn = document.querySelector('.ytp-next-button') || document.querySelector(' [aria-label*="Next"]');
310+ const nextBtn = document.querySelector('.ytp-next-button, [aria-label*="Next"]');
358311 if (nextBtn) {
359312 nextBtn.click();
360313 } else {
@@ -370,84 +323,59 @@ object FingerprintScriptGenerator {
370323 return;
371324 }
372325 } catch(e) {}
373- const prevBtn = document.querySelector('.ytp-prev-button') || document.querySelector(' [aria-label*="Previous"]');
326+ const prevBtn = document.querySelector('.ytp-prev-button, [aria-label*="Previous"]');
374327 if (prevBtn) {
375328 prevBtn.click();
376329 } else {
377330 window.history.back();
378331 }
379332 };
380333
381- // 8. Hook HTMLMediaElement prototype play and pause with bulletproof background defense
382- let origPlay = null;
383- let origPause = null;
384- try {
385- origPlay = HTMLMediaElement.prototype.play;
386- HTMLMediaElement.prototype.play = function() {
387- window.__feather_explicit_pause = false;
388- notifyMediaBridge(true);
389- return origPlay.apply(this, arguments);
390- };
334+ // 5. Periodic monitor & watchdog for HTML media elements
335+ let lastReportedState = null;
336+ let lastReportedTitle = '';
391337
392- origPause = HTMLMediaElement.prototype.pause;
393- HTMLMediaElement.prototype.pause = function() {
394- const isAllowedPause = window.__feather_explicit_pause || userInteractedWithPlayer;
395- if (!isAllowedPause) {
396- // Suppress unwanted auto-pause triggered by backgrounding or visibility change!
397- return;
338+ function hookMediaElement(el) {
339+ if (!el || el.__feather_monitored) return;
340+ el.__feather_monitored = true;
341+
342+ ['play', 'playing'].forEach(function(evt) {
343+ el.addEventListener(evt, function() {
344+ wasActivePlayback = true;
345+ notifyMediaBridge(true);
346+ });
347+ });
348+
349+ el.addEventListener('pause', function() {
350+ const anyStillPlaying = Array.from(document.querySelectorAll('video, audio')).some(function(m) {
351+ return !m.paused && !m.ended;
352+ });
353+ if (!anyStillPlaying) {
354+ notifyMediaBridge(false);
398355 }
399- window.__feather_explicit_pause = false;
400- userInteractedWithPlayer = false;
401- notifyMediaBridge(false);
402- return origPause.apply(this, arguments);
403- };
404- } catch(e) {}
356+ });
405357
406- // 9. Periodic monitor & watchdog for HTML media elements
407- let lastReportedState = null;
408- let lastReportedTitle = '';
358+ el.addEventListener('ended', function() {
359+ wasActivePlayback = false;
360+ if (window.FeatherMediaBridge) {
361+ window.FeatherMediaBridge.onMediaEnded();
362+ }
363+ });
364+ }
409365
410366 const monitorMedia = function() {
411367 try {
412368 const els = document.querySelectorAll('video, audio');
413369 let anyPlaying = false;
414370 els.forEach(function(el) {
371+ hookMediaElement(el);
415372 if (!el.paused && !el.ended) {
416373 anyPlaying = true;
417374 }
418- if (el.__feather_monitored) return;
419- el.__feather_monitored = true;
420-
421- el.addEventListener('play', function() {
422- wasActivePlayback = true;
423- notifyMediaBridge(true);
424- });
425-
426- el.addEventListener('pause', function() {
427- if (window.__feather_explicit_pause || userInteractedWithPlayer) {
428- notifyMediaBridge(false);
429- } else if (wasActivePlayback) {
430- // Auto-resume if backgrounding caused an unexpected pause
431- el.play().catch(function() {});
432- }
433- });
434-
435- el.addEventListener('ended', function() {
436- wasActivePlayback = false;
437- if (window.FeatherMediaBridge) {
438- window.FeatherMediaBridge.onMediaEnded();
439- }
440- });
441375 });
442376
443377 if (anyPlaying) {
444378 wasActivePlayback = true;
445- } else if (wasActivePlayback && !window.__feather_explicit_pause && !userInteractedWithPlayer) {
446- // Automatically resume background playback if video paused without user interaction
447- const video = document.querySelector('video');
448- if (video && video.paused && !video.ended) {
449- video.play().catch(function() {});
450- }
451379 }
452380
453381 const currentTitle = getMediaTitle();
@@ -460,7 +388,7 @@ object FingerprintScriptGenerator {
460388 }
461389 } catch(e) {}
462390 };
463- setInterval(monitorMedia, 800 );
391+ setInterval(monitorMedia, 1000 );
464392 monitorMedia();
465393 } catch(e) {}
466394 })();
0 commit comments