@@ -79,32 +79,26 @@ object FingerprintScriptGenerator {
7979 window.__feather_bg_play_active = true;
8080
8181 // 1. Spoof Page Visibility API so websites (like YouTube) report visible and focused
82- try {
83- const defineProp = function(obj, prop, value) {
84- try {
85- Object.defineProperty(obj, prop, {
86- get: function() { return value; },
87- set: function() {},
88- configurable: true,
89- enumerable: true
90- });
91- } catch(e) {}
92- };
82+ ['hidden', 'webkitHidden'].forEach(function(prop) {
83+ try {
84+ Object.defineProperty(document, prop, { get: function() { return false; }, configurable: true, enumerable: true });
85+ Object.defineProperty(Document.prototype, prop, { get: function() { return false; }, configurable: true, enumerable: true });
86+ } catch(e) {}
87+ });
9388
94- defineProp(document, 'hidden', false);
95- defineProp(document, 'visibilityState', 'visible');
96- defineProp(document, 'webkitHidden', false);
97- defineProp(document, 'webkitVisibilityState', 'visible');
98- defineProp(Document.prototype, 'hidden', false);
99- defineProp(Document.prototype, 'visibilityState', 'visible');
100- defineProp(Document.prototype, 'webkitHidden', false);
101- defineProp(Document.prototype, 'webkitVisibilityState', 'visible');
102-
89+ ['visibilityState', 'webkitVisibilityState'].forEach(function(prop) {
90+ try {
91+ Object.defineProperty(document, prop, { get: function() { return 'visible'; }, configurable: true, enumerable: true });
92+ Object.defineProperty(Document.prototype, prop, { get: function() { return 'visible'; }, configurable: true, enumerable: true });
93+ } catch(e) {}
94+ });
95+
96+ try {
10397 document.hasFocus = function() { return true; };
10498 Document.prototype.hasFocus = function() { return true; };
10599 } catch(e) {}
106100
107- // 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange event listeners
101+ // 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange and pagehide
108102 try {
109103 const origAddEventListener = EventTarget.prototype.addEventListener;
110104 EventTarget.prototype.addEventListener = function(type, listener, options) {
@@ -114,19 +108,50 @@ object FingerprintScriptGenerator {
114108 return origAddEventListener.apply(this, arguments);
115109 };
116110
117- ['visibilitychange', 'webkitvisibilitychange'].forEach(function(evt) {
118- origAddEventListener.call(window, evt, function(e) {
111+ ['visibilitychange', 'webkitvisibilitychange', 'pagehide' ].forEach(function(evt) {
112+ window.addEventListener( evt, function(e) {
119113 if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
120114 if (e && e.stopPropagation) e.stopPropagation();
121115 }, true);
122- origAddEventListener.call(document, evt, function(e) {
116+ document.addEventListener( evt, function(e) {
123117 if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
124118 if (e && e.stopPropagation) e.stopPropagation();
125119 }, true);
126120 });
127121 } catch(e) {}
128122
129- // 3. Media Session Hooking for Android notification metadata & controls
123+ // 3. Spoof IntersectionObserver for video / player elements so YouTube mobile doesn't pause when offscreen
124+ try {
125+ if ('IntersectionObserver' in window) {
126+ const OrigObserver = window.IntersectionObserver;
127+ window.IntersectionObserver = function(callback, options) {
128+ const wrappedCallback = function(entries, obs) {
129+ try {
130+ const modified = entries.map(function(entry) {
131+ const target = entry.target;
132+ if (target && (target.tagName === 'VIDEO' || target.id === 'player' || (target.className && typeof target.className === 'string' && (target.className.indexOf('player') !== -1 || target.className.indexOf('video') !== -1)))) {
133+ return new Proxy(entry, {
134+ get: function(t, p) {
135+ if (p === 'isIntersecting') return true;
136+ if (p === 'intersectionRatio') return 1.0;
137+ return t[p];
138+ }
139+ });
140+ }
141+ return entry;
142+ });
143+ return callback(modified, obs);
144+ } catch(e) {
145+ return callback(entries, obs);
146+ }
147+ };
148+ return new OrigObserver(wrappedCallback, options);
149+ };
150+ window.IntersectionObserver.prototype = OrigObserver.prototype;
151+ }
152+ } catch(e) {}
153+
154+ // 4. Media Session Hooking for Android notification metadata & controls
130155 window.__feather_actions = window.__feather_actions || {};
131156
132157 function getMediaThumbnail() {
@@ -147,22 +172,22 @@ object FingerprintScriptGenerator {
147172 if (navigator.mediaSession && navigator.mediaSession.metadata && navigator.mediaSession.metadata.title) {
148173 return navigator.mediaSession.metadata.title;
149174 }
150- const ytTitle = document.querySelector('h1.title, .slim-video-metadata-title, ytm-slim-video-metadata-section-renderer .slim-video-information-title, ytd-watch-metadata #title h1, .ytp-title-link');
175+ const ytTitle = document.querySelector('h1.title, .slim-video-metadata-title, ytm-slim-video-metadata-section-renderer .slim-video-information-title, ytd-watch-metadata #title h1, .ytp-title-link, [class*="video-title"] ');
151176 if (ytTitle && ytTitle.innerText && ytTitle.innerText.trim()) {
152177 return ytTitle.innerText.trim();
153178 }
154179 const docTitle = document.title.replace(/ - YouTube$/i, '').replace(/^\(\d+\)\s*/, '').trim();
155180 if (docTitle && docTitle !== 'YouTube') return docTitle;
156181 } catch(e) {}
157- return 'YouTube';
182+ return 'YouTube Video ';
158183 }
159184
160185 function getMediaArtist() {
161186 try {
162187 if (navigator.mediaSession && navigator.mediaSession.metadata && navigator.mediaSession.metadata.artist) {
163188 return navigator.mediaSession.metadata.artist;
164189 }
165- const ytAuthor = document.querySelector('.ytm-channel-thumbnail-with-profile-name .profile-name, #owner-name a, #channel-name a, ytd-channel-name a, .ytm-media-item-metadata .channel-name');
190+ const ytAuthor = document.querySelector('.ytm-channel-thumbnail-with-profile-name .profile-name, #owner-name a, #channel-name a, ytd-channel-name a, .ytm-media-item-metadata .channel-name, [class*="channel-name"] ');
166191 if (ytAuthor && ytAuthor.innerText && ytAuthor.innerText.trim()) {
167192 return ytAuthor.innerText.trim();
168193 }
@@ -175,59 +200,79 @@ object FingerprintScriptGenerator {
175200 function notifyMediaBridge(isPlaying) {
176201 try {
177202 if (!window.FeatherMediaBridge) return;
178- if (!isPlaying && !wasActivePlayback) return;
179-
180203 if (isPlaying) {
181204 wasActivePlayback = true;
182205 const title = getMediaTitle();
183206 const artist = getMediaArtist();
184207 const art = getMediaThumbnail();
185- window.FeatherMediaBridge.updateMetadata(title, artist, 'Feather Browser', art);
208+ window.FeatherMediaBridge.updateMetadata(title, artist, 'YouTube', art);
209+ window.FeatherMediaBridge.updatePlaybackState(true);
210+ } else {
211+ if (wasActivePlayback) {
212+ window.FeatherMediaBridge.updatePlaybackState(false);
213+ }
186214 }
187- window.FeatherMediaBridge.updatePlaybackState(isPlaying);
188215 } catch(e) {}
189216 }
190217
191218 try {
192219 if ('mediaSession' in navigator) {
193- const origMS = navigator.mediaSession;
220+ const ms = navigator.mediaSession;
221+ let _currentMetadata = ms.metadata || null;
222+
194223 try {
195- const origSetMetadata = Object.getOwnPropertyDescriptor(MediaSession.prototype, 'metadata')?.set;
196- if (origSetMetadata) {
197- Object.defineProperty(origMS, 'metadata', {
198- set: function(val) {
199- try {
200- if (window.FeatherMediaBridge && val) {
201- let artUrl = '';
202- if (val.artwork && val.artwork.length > 0) {
203- artUrl = val.artwork[val.artwork.length - 1].src || '';
204- }
205- window.FeatherMediaBridge.updateMetadata(
206- val.title || getMediaTitle(),
207- val.artist || getMediaArtist(),
208- val.album || 'Feather Browser',
209- artUrl || getMediaThumbnail()
210- );
224+ const proto = Object.getPrototypeOf(ms) || ms;
225+ const desc = Object.getOwnPropertyDescriptor(proto, 'metadata') || Object.getOwnPropertyDescriptor(ms, 'metadata');
226+ Object.defineProperty(ms, 'metadata', {
227+ get: function() {
228+ if (desc && desc.get) {
229+ try { return desc.get.call(this); } catch(e) {}
230+ }
231+ return _currentMetadata;
232+ },
233+ set: function(val) {
234+ _currentMetadata = val;
235+ if (desc && desc.set) {
236+ try { desc.set.call(this, val); } catch(e) {}
237+ }
238+ try {
239+ if (window.FeatherMediaBridge && val) {
240+ let artUrl = '';
241+ if (val.artwork && val.artwork.length > 0) {
242+ artUrl = val.artwork[val.artwork.length - 1].src || '';
211243 }
212- } catch(e) {}
213- return origSetMetadata.call(this, val);
214- },
215- configurable: true
216- });
217- }
244+ const t = val.title || getMediaTitle();
245+ const a = val.artist || getMediaArtist();
246+ window.FeatherMediaBridge.updateMetadata(
247+ t,
248+ a,
249+ val.album || 'YouTube',
250+ artUrl || getMediaThumbnail()
251+ );
252+ window.FeatherMediaBridge.updatePlaybackState(true);
253+ }
254+ } catch(e) {}
255+ },
256+ configurable: true,
257+ enumerable: true
258+ });
218259 } catch(e) {}
219260
220261 try {
221- const origSetHandler = origMS.setActionHandler.bind(origMS);
222- origMS.setActionHandler = function(action, handler) {
223- window.__feather_actions[action] = handler;
224- return origSetHandler(action, handler);
225- };
262+ const origSetHandler = ms.setActionHandler;
263+ if (typeof origSetHandler === 'function') {
264+ ms.setActionHandler = function(action, handler) {
265+ window.__feather_actions[action] = handler;
266+ try {
267+ return origSetHandler.call(ms, action, handler);
268+ } catch(e) {}
269+ };
270+ }
226271 } catch(e) {}
227272 }
228273 } catch(e) {}
229274
230- // 4 . Media control methods callable from notification bar
275+ // 5 . Media control methods callable from notification bar
231276 window.__feather_media_play = function() {
232277 try {
233278 if (window.__feather_actions && typeof window.__feather_actions['play'] === 'function') {
@@ -331,28 +376,37 @@ object FingerprintScriptGenerator {
331376 }
332377 };
333378
334- // 5 . Periodic monitor & watchdog for HTML media elements
379+ // 6 . Periodic monitor & watchdog for HTML media elements
335380 let lastReportedState = null;
336381 let lastReportedTitle = '';
337382
338383 function hookMediaElement(el) {
339384 if (!el || el.__feather_monitored) return;
340385 el.__feather_monitored = true;
341386
342- ['play', 'playing'].forEach(function(evt) {
343- el.addEventListener(evt, function() {
387+ function handlePlay() {
388+ wasActivePlayback = true;
389+ notifyMediaBridge(true);
390+ }
391+
392+ el.addEventListener('play', handlePlay);
393+ el.addEventListener('playing', handlePlay);
394+ el.addEventListener('timeupdate', function() {
395+ if (!el.paused && !el.ended && !wasActivePlayback) {
344396 wasActivePlayback = true;
345397 notifyMediaBridge(true);
346- });
398+ }
347399 });
348400
349401 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);
355- }
402+ setTimeout(function() {
403+ const anyPlaying = Array.from(document.querySelectorAll('video, audio')).some(function(m) {
404+ return !m.paused && !m.ended;
405+ });
406+ if (!anyPlaying) {
407+ notifyMediaBridge(false);
408+ }
409+ }, 250);
356410 });
357411
358412 el.addEventListener('ended', function() {
@@ -369,7 +423,7 @@ object FingerprintScriptGenerator {
369423 let anyPlaying = false;
370424 els.forEach(function(el) {
371425 hookMediaElement(el);
372- if (!el.paused && !el.ended) {
426+ if (!el.paused && !el.ended && el.currentTime > 0 ) {
373427 anyPlaying = true;
374428 }
375429 });
@@ -388,7 +442,7 @@ object FingerprintScriptGenerator {
388442 }
389443 } catch(e) {}
390444 };
391- setInterval(monitorMedia, 1000 );
445+ setInterval(monitorMedia, 800 );
392446 monitorMedia();
393447 } catch(e) {}
394448 })();
0 commit comments