Last active
April 24, 2026 01:22
-
-
Save affan2021shaikh/efa8b9f8c31b794d2d3b2a1b99928e0f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name DarkReader ES Module Equivalent | |
| // @namespace affan2021shaikh | |
| // @version 1.0 | |
| // @description Use DarkReader in a cross-manager compatible way | |
| // @author ChatGPT 5.3 | |
| // @downloadURL https://gist.github.com/affan2021shaikh/efa8b9f8c31b794d2d3b2a1b99928e0f/raw/DarkReader%2520ES%2520Module%2520Equivalent.user.js | |
| // @match *://*/* | |
| // @grant none | |
| // @require https://unpkg.com/darkreader/darkreader.min.js | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // DarkReader is exposed globally | |
| const { | |
| enable: enableDarkMode, | |
| disable: disableDarkMode, | |
| auto: followSystemColorScheme, | |
| exportGeneratedCSS: collectCSS, | |
| isEnabled: isDarkReaderEnabled | |
| } = DarkReader; | |
| // Enable dark mode | |
| enableDarkMode({ | |
| brightness: 100, | |
| contrast: 90, | |
| sepia: 10, | |
| }); | |
| // Disable (example call) | |
| disableDarkMode(); | |
| // Follow system scheme | |
| followSystemColorScheme(); | |
| // Async call wrapped properly (no top-level await) | |
| (async () => { | |
| const CSS = await collectCSS(); | |
| console.log('Generated CSS:', CSS); | |
| const isEnabled = isDarkReaderEnabled(); | |
| console.log('Is enabled:', isEnabled); | |
| })(); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name YouTube Background Play Max | |
| // @namespace affan2021shaikh | |
| // @version 3.0.0 | |
| // @description Best-effort background play for YouTube desktop and mobile web using only standard userscript features. | |
| // @author ChatGPT 5.3 Thinking | |
| // @downloadURL https://gist.github.com/affan2021shaikh/efa8b9f8c31b794d2d3b2a1b99928e0f/raw/YouTube%2520Background%2520Play%2520Max.user.js | |
| // @match https://www.youtube.com/* | |
| // @match https://m.youtube.com/* | |
| // @run-at document-start | |
| // @grant none | |
| // @noframes | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const CFG = { | |
| debug: false, | |
| manualGraceMs: 1200, | |
| hiddenRetryMs: 200, | |
| visibleRetryMs: 350, | |
| maxRetryMs: 5000, | |
| hiddenRetryWindowMs: 30000, | |
| visibilitySpoof: true, | |
| mediaSession: true, | |
| audioKeepalive: true, | |
| pipFallback: true, | |
| mutedFallback: true, | |
| pipRetryCooldownMs: 15000, | |
| mutedRetryCooldownMs: 10000, | |
| restoreMuteAfterMs: 1200, | |
| }; | |
| const log = (...args) => { | |
| if (CFG.debug) console.log('[YT BG Play Max]', ...args); | |
| }; | |
| const SEL = [ | |
| '#movie_player video', | |
| 'ytd-player video', | |
| 'video.html5-main-video', | |
| 'video', | |
| ]; | |
| const CONTROL_SEL = [ | |
| '.ytp-play-button', | |
| '.ytp-big-mode-play-button', | |
| 'button', | |
| '[role="button"]', | |
| 'tp-yt-paper-icon-button', | |
| 'yt-icon-button', | |
| ].join(','); | |
| const STATE = { | |
| currentVideo: null, | |
| desiredPlaying: false, | |
| lastGestureAt: 0, | |
| lastExplicitPlayPauseAt: 0, | |
| lastManualPauseAt: 0, | |
| hiddenSince: 0, | |
| retryTimer: 0, | |
| retryDelay: CFG.visibleRetryMs, | |
| playInFlight: false, | |
| booted: false, | |
| observerStarted: false, | |
| frameLoopStarted: false, | |
| failures: 0, | |
| lastPipAttemptAt: 0, | |
| lastMutedFallbackAt: 0, | |
| media: { | |
| ctx: null, | |
| oscillator: null, | |
| gain: null, | |
| unlocked: false, | |
| }, | |
| originals: { | |
| hidden: null, | |
| visibilityState: null, | |
| webkitHidden: null, | |
| webkitVisibilityState: null, | |
| hasFocus: null, | |
| }, | |
| }; | |
| const now = () => Date.now(); | |
| function isElement(v) { | |
| return v && v.nodeType === 1; | |
| } | |
| function isTypingTarget(target) { | |
| if (!isElement(target)) return false; | |
| const tag = target.tagName; | |
| return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable; | |
| } | |
| function getDescriptor(proto, prop) { | |
| let p = proto; | |
| while (p) { | |
| const d = Object.getOwnPropertyDescriptor(p, prop); | |
| if (d) return d; | |
| p = Object.getPrototypeOf(p); | |
| } | |
| return null; | |
| } | |
| function readActualHidden() { | |
| try { | |
| const d = STATE.originals.hidden; | |
| if (d && typeof d.get === 'function') return !!d.get.call(document); | |
| } catch (_) {} | |
| try { | |
| const d = STATE.originals.webkitHidden; | |
| if (d && typeof d.get === 'function') return !!d.get.call(document); | |
| } catch (_) {} | |
| try { | |
| return document.visibilityState !== 'visible'; | |
| } catch (_) { | |
| return false; | |
| } | |
| } | |
| function readActualVisibilityState() { | |
| try { | |
| const d = STATE.originals.visibilityState; | |
| if (d && typeof d.get === 'function') return String(d.get.call(document)); | |
| } catch (_) {} | |
| try { | |
| const d = STATE.originals.webkitVisibilityState; | |
| if (d && typeof d.get === 'function') return String(d.get.call(document)); | |
| } catch (_) {} | |
| return readActualHidden() ? 'hidden' : 'visible'; | |
| } | |
| function isActuallyHidden() { | |
| return readActualVisibilityState() !== 'visible'; | |
| } | |
| function installVisibilitySpoof() { | |
| if (!CFG.visibilitySpoof) return; | |
| const proto = Document.prototype; | |
| STATE.originals.hidden = getDescriptor(proto, 'hidden'); | |
| STATE.originals.visibilityState = getDescriptor(proto, 'visibilityState'); | |
| STATE.originals.webkitHidden = getDescriptor(proto, 'webkitHidden'); | |
| STATE.originals.webkitVisibilityState = getDescriptor(proto, 'webkitVisibilityState'); | |
| STATE.originals.hasFocus = getDescriptor(proto, 'hasFocus'); | |
| const installGetter = (prop, getter, original) => { | |
| try { | |
| if (original && original.configurable === false) return false; | |
| Object.defineProperty(proto, prop, { | |
| configurable: true, | |
| enumerable: original ? !!original.enumerable : false, | |
| get: getter, | |
| }); | |
| return true; | |
| } catch (_) { | |
| return false; | |
| } | |
| }; | |
| installGetter('hidden', function () { | |
| return false; | |
| }, STATE.originals.hidden); | |
| installGetter('visibilityState', function () { | |
| return 'visible'; | |
| }, STATE.originals.visibilityState); | |
| installGetter('webkitHidden', function () { | |
| return false; | |
| }, STATE.originals.webkitHidden); | |
| installGetter('webkitVisibilityState', function () { | |
| return 'visible'; | |
| }, STATE.originals.webkitVisibilityState); | |
| try { | |
| const original = STATE.originals.hasFocus; | |
| if (!original || original.configurable !== false) { | |
| Object.defineProperty(proto, 'hasFocus', { | |
| configurable: true, | |
| enumerable: original ? !!original.enumerable : false, | |
| writable: true, | |
| value: function () { | |
| return true; | |
| }, | |
| }); | |
| } | |
| } catch (_) {} | |
| } | |
| function setMediaSessionState() { | |
| if (!CFG.mediaSession) return; | |
| try { | |
| if (!('mediaSession' in navigator)) return; | |
| navigator.mediaSession.playbackState = STATE.desiredPlaying ? 'playing' : 'paused'; | |
| } catch (_) {} | |
| } | |
| function stopKeepaliveAudio() { | |
| const m = STATE.media; | |
| try { | |
| if (m.ctx && m.ctx.state === 'running') { | |
| m.ctx.suspend().catch(() => {}); | |
| } | |
| } catch (_) {} | |
| } | |
| function startKeepaliveAudio() { | |
| if (!CFG.audioKeepalive) return; | |
| if (!STATE.desiredPlaying) return; | |
| if (!isActuallyHidden()) { | |
| stopKeepaliveAudio(); | |
| return; | |
| } | |
| const m = STATE.media; | |
| try { | |
| const AudioCtx = window.AudioContext || window.webkitAudioContext; | |
| if (!AudioCtx) return; | |
| if (!m.ctx) { | |
| m.ctx = new AudioCtx(); | |
| m.gain = m.ctx.createGain(); | |
| m.gain.gain.value = 0.00001; | |
| m.oscillator = m.ctx.createOscillator(); | |
| m.oscillator.type = 'sine'; | |
| m.oscillator.frequency.value = 40; | |
| m.oscillator.connect(m.gain); | |
| m.gain.connect(m.ctx.destination); | |
| m.oscillator.start(); | |
| } | |
| if (m.ctx.state === 'suspended') { | |
| m.ctx.resume().catch(() => {}); | |
| } | |
| } catch (_) {} | |
| } | |
| function unlockKeepaliveAudio() { | |
| if (!CFG.audioKeepalive) return; | |
| const m = STATE.media; | |
| m.unlocked = true; | |
| if (m.ctx && m.ctx.state === 'suspended') { | |
| m.ctx.resume().catch(() => {}); | |
| } | |
| } | |
| function maybeStartKeepaliveAudio() { | |
| if (!CFG.audioKeepalive) return; | |
| if (!STATE.media.unlocked) return; | |
| startKeepaliveAudio(); | |
| } | |
| function setDesiredPlaying(next, reason) { | |
| const changed = STATE.desiredPlaying !== next; | |
| STATE.desiredPlaying = next; | |
| if (next) { | |
| setMediaSessionState(); | |
| maybeStartKeepaliveAudio(); | |
| if (changed) scheduleRetry(reason || 'intent', true); | |
| } else { | |
| setMediaSessionState(); | |
| stopKeepaliveAudio(); | |
| clearTimeout(STATE.retryTimer); | |
| } | |
| } | |
| function manualGraceActive() { | |
| const t = now(); | |
| const recent = Math.max(STATE.lastGestureAt, STATE.lastExplicitPlayPauseAt, STATE.lastManualPauseAt); | |
| return (t - recent) <= CFG.manualGraceMs; | |
| } | |
| function touchGesture(event) { | |
| STATE.lastGestureAt = now(); | |
| if (isTypingTarget(event && event.target)) return; | |
| unlockKeepaliveAudio(); | |
| } | |
| function inferControlIntent(event) { | |
| const target = event && event.target; | |
| if (!isElement(target)) return null; | |
| const control = target.closest('.ytp-play-button, .ytp-big-mode-play-button'); | |
| if (control) { | |
| const label = [ | |
| control.getAttribute('aria-label'), | |
| control.getAttribute('title'), | |
| control.getAttribute('data-title-no-tooltip'), | |
| control.textContent, | |
| ].filter(Boolean).join(' ').toLowerCase(); | |
| if (label.includes('pause') && !label.includes('play')) return 'pause'; | |
| if (label.includes('play') && !label.includes('pause')) return 'play'; | |
| if (STATE.currentVideo) { | |
| return STATE.currentVideo.paused ? 'play' : 'pause'; | |
| } | |
| } | |
| return null; | |
| } | |
| function onPointerLike(event) { | |
| touchGesture(event); | |
| const intent = inferControlIntent(event); | |
| if (intent === 'play') { | |
| STATE.lastExplicitPlayPauseAt = now(); | |
| setDesiredPlaying(true, 'user-play'); | |
| } else if (intent === 'pause') { | |
| STATE.lastExplicitPlayPauseAt = now(); | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'user-pause'); | |
| } | |
| } | |
| function onKeydown(event) { | |
| if (event.repeat) return; | |
| if (isTypingTarget(event.target)) return; | |
| touchGesture(event); | |
| const key = String(event.key || '').toLowerCase(); | |
| const mediaKey = key === 'mediaplaypause' || key === 'mediaplay' || key === 'play'; | |
| const pauseKey = key === 'mediapause'; | |
| if (mediaKey) { | |
| STATE.lastExplicitPlayPauseAt = now(); | |
| setDesiredPlaying(true, 'media-key-play'); | |
| return; | |
| } | |
| if (pauseKey) { | |
| STATE.lastExplicitPlayPauseAt = now(); | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'media-key-pause'); | |
| return; | |
| } | |
| if (key === ' ' || key === 'spacebar' || key === 'k') { | |
| STATE.lastExplicitPlayPauseAt = now(); | |
| if (STATE.currentVideo && !STATE.currentVideo.paused) { | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'toggle-pause'); | |
| } else { | |
| setDesiredPlaying(true, 'toggle-play'); | |
| } | |
| } | |
| } | |
| function scoreVideo(v) { | |
| if (!(v instanceof HTMLVideoElement) || !v.isConnected) return -1e18; | |
| const r = v.getBoundingClientRect(); | |
| const area = Math.max(0, r.width) * Math.max(0, r.height); | |
| let score = area; | |
| if (r.width > 0 && r.height > 0) score += 1_000_000; | |
| if (!v.paused) score += 250_000; | |
| if (v.readyState >= 2) score += 15_000; | |
| if (v.currentSrc) score += 5_000; | |
| if (document.body && document.body.contains(v)) score += 2_000; | |
| if (v.matches('.html5-main-video')) score += 250_000; | |
| if (v.closest('#movie_player')) score += 175_000; | |
| if (v.closest('ytd-player')) score += 125_000; | |
| if (v.closest('.html5-video-container')) score += 75_000; | |
| return score; | |
| } | |
| function pickVideo() { | |
| const seen = new Set(); | |
| const candidates = []; | |
| for (const sel of SEL) { | |
| for (const v of document.querySelectorAll(sel)) { | |
| if (!seen.has(v)) { | |
| seen.add(v); | |
| candidates.push(v); | |
| } | |
| } | |
| } | |
| let best = null; | |
| let bestScore = -1e18; | |
| for (const v of candidates) { | |
| const s = scoreVideo(v); | |
| if (s > bestScore) { | |
| bestScore = s; | |
| best = v; | |
| } | |
| } | |
| return best; | |
| } | |
| function attachVideo(v) { | |
| if (!(v instanceof HTMLVideoElement)) return; | |
| if (v === STATE.currentVideo) return; | |
| if (STATE.currentVideo) { | |
| STATE.currentVideo.removeEventListener('play', onPlay, true); | |
| STATE.currentVideo.removeEventListener('playing', onPlaying, true); | |
| STATE.currentVideo.removeEventListener('pause', onPause, true); | |
| STATE.currentVideo.removeEventListener('ended', onEnded, true); | |
| STATE.currentVideo.removeEventListener('emptied', onVideoSourceChange, true); | |
| STATE.currentVideo.removeEventListener('loadstart', onVideoSourceChange, true); | |
| STATE.currentVideo.removeEventListener('loadedmetadata', onVideoSourceChange, true); | |
| STATE.currentVideo.removeEventListener('stalled', onNeedRetry, true); | |
| STATE.currentVideo.removeEventListener('waiting', onNeedRetry, true); | |
| STATE.currentVideo.removeEventListener('suspend', onNeedRetry, true); | |
| STATE.currentVideo.removeEventListener('canplay', onNeedRetry, true); | |
| } | |
| STATE.currentVideo = v; | |
| try { | |
| STATE.currentVideo.setAttribute('playsinline', ''); | |
| STATE.currentVideo.setAttribute('webkit-playsinline', ''); | |
| } catch (_) {} | |
| STATE.currentVideo.addEventListener('play', onPlay, true); | |
| STATE.currentVideo.addEventListener('playing', onPlaying, true); | |
| STATE.currentVideo.addEventListener('pause', onPause, true); | |
| STATE.currentVideo.addEventListener('ended', onEnded, true); | |
| STATE.currentVideo.addEventListener('emptied', onVideoSourceChange, true); | |
| STATE.currentVideo.addEventListener('loadstart', onVideoSourceChange, true); | |
| STATE.currentVideo.addEventListener('loadedmetadata', onVideoSourceChange, true); | |
| STATE.currentVideo.addEventListener('stalled', onNeedRetry, true); | |
| STATE.currentVideo.addEventListener('waiting', onNeedRetry, true); | |
| STATE.currentVideo.addEventListener('suspend', onNeedRetry, true); | |
| STATE.currentVideo.addEventListener('canplay', onNeedRetry, true); | |
| log('attached video'); | |
| } | |
| function syncVideo() { | |
| const best = pickVideo(); | |
| if (best) attachVideo(best); | |
| } | |
| function resetRetryDelay() { | |
| STATE.retryDelay = isActuallyHidden() ? CFG.hiddenRetryMs : CFG.visibleRetryMs; | |
| } | |
| function bumpRetryDelay() { | |
| STATE.retryDelay = Math.min(Math.max(STATE.retryDelay * 1.5, 200), CFG.maxRetryMs); | |
| } | |
| async function tryPictureInPicture(reason) { | |
| if (!CFG.pipFallback) return false; | |
| if (!STATE.currentVideo || !STATE.desiredPlaying) return false; | |
| if (!STATE.currentVideo.isConnected) return false; | |
| if (!(location.hostname.startsWith('m.') || matchMedia('(pointer: coarse)').matches)) return false; | |
| if (!document.pictureInPictureEnabled) return false; | |
| if (document.pictureInPictureElement === STATE.currentVideo) return true; | |
| if (STATE.currentVideo.disablePictureInPicture) return false; | |
| const t = now(); | |
| if (t - STATE.lastPipAttemptAt < CFG.pipRetryCooldownMs) return false; | |
| STATE.lastPipAttemptAt = t; | |
| try { | |
| if (typeof STATE.currentVideo.requestPictureInPicture !== 'function') return false; | |
| await STATE.currentVideo.requestPictureInPicture(); | |
| log('pip ok', reason); | |
| return true; | |
| } catch (err) { | |
| log('pip failed', reason, err && err.name ? err.name : err); | |
| return false; | |
| } | |
| } | |
| async function tryMutedFallback(reason) { | |
| if (!CFG.mutedFallback) return false; | |
| if (!STATE.currentVideo || !STATE.desiredPlaying) return false; | |
| if (!STATE.currentVideo.isConnected) return false; | |
| if (STATE.currentVideo.muted) return false; | |
| const t = now(); | |
| if (t - STATE.lastMutedFallbackAt < CFG.mutedRetryCooldownMs) return false; | |
| STATE.lastMutedFallbackAt = t; | |
| const prevMuted = STATE.currentVideo.muted; | |
| const prevVolume = STATE.currentVideo.volume; | |
| try { | |
| STATE.currentVideo.muted = true; | |
| const p = STATE.currentVideo.play(); | |
| if (p && typeof p.then === 'function') { | |
| await p; | |
| } | |
| setTimeout(() => { | |
| try { | |
| if (!STATE.currentVideo || STATE.currentVideo.paused) return; | |
| if (!STATE.desiredPlaying) return; | |
| STATE.currentVideo.muted = prevMuted; | |
| STATE.currentVideo.volume = prevVolume; | |
| } catch (_) {} | |
| }, CFG.restoreMuteAfterMs); | |
| log('muted fallback ok', reason); | |
| return true; | |
| } catch (err) { | |
| try { | |
| if (STATE.currentVideo) { | |
| STATE.currentVideo.muted = prevMuted; | |
| STATE.currentVideo.volume = prevVolume; | |
| } | |
| } catch (_) {} | |
| log('muted fallback failed', reason, err && err.name ? err.name : err); | |
| return false; | |
| } | |
| } | |
| async function safePlay(reason) { | |
| if (!STATE.currentVideo || !STATE.desiredPlaying) return false; | |
| if (STATE.playInFlight) return true; | |
| STATE.playInFlight = true; | |
| try { | |
| if (!STATE.currentVideo.isConnected) { | |
| syncVideo(); | |
| if (!STATE.currentVideo) return false; | |
| } | |
| if (!STATE.currentVideo.paused && STATE.currentVideo.readyState >= 2) { | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| maybeStartKeepaliveAudio(); | |
| setMediaSessionState(); | |
| return true; | |
| } | |
| try { | |
| const p = STATE.currentVideo.play(); | |
| if (p && typeof p.then === 'function') { | |
| await p; | |
| } | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| maybeStartKeepaliveAudio(); | |
| setMediaSessionState(); | |
| log('play ok', reason); | |
| return true; | |
| } catch (err) { | |
| log('play failed', reason, err && err.name ? err.name : err); | |
| } | |
| STATE.failures += 1; | |
| bumpRetryDelay(); | |
| if (STATE.failures >= 2) { | |
| const pipOk = await tryPictureInPicture(reason); | |
| if (pipOk) { | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| maybeStartKeepaliveAudio(); | |
| setMediaSessionState(); | |
| return true; | |
| } | |
| } | |
| if (STATE.failures >= 3) { | |
| const mutedOk = await tryMutedFallback(reason); | |
| if (mutedOk) { | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| maybeStartKeepaliveAudio(); | |
| setMediaSessionState(); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } finally { | |
| STATE.playInFlight = false; | |
| } | |
| } | |
| function scheduleRetry(reason, immediate = false) { | |
| if (!STATE.desiredPlaying) return; | |
| clearTimeout(STATE.retryTimer); | |
| const hidden = isActuallyHidden(); | |
| const delay = immediate ? 0 : (hidden ? STATE.hiddenRetryDelay || CFG.hiddenRetryMs : STATE.retryDelay); | |
| STATE.retryTimer = setTimeout(async () => { | |
| if (!STATE.desiredPlaying) return; | |
| const stillHidden = isActuallyHidden(); | |
| if (stillHidden && STATE.hiddenSince && (now() - STATE.hiddenSince) > CFG.hiddenRetryWindowMs) { | |
| return; | |
| } | |
| syncVideo(); | |
| if (!STATE.currentVideo) { | |
| bumpRetryDelay(); | |
| scheduleRetry(reason); | |
| return; | |
| } | |
| if (!STATE.currentVideo.paused && STATE.currentVideo.readyState >= 2) { | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| maybeStartKeepaliveAudio(); | |
| return; | |
| } | |
| const ok = await safePlay(reason); | |
| if (!ok && STATE.desiredPlaying) { | |
| scheduleRetry(reason); | |
| } | |
| }, delay); | |
| } | |
| function onPlay() { | |
| setDesiredPlaying(true, 'play-event'); | |
| } | |
| function onPlaying() { | |
| setDesiredPlaying(true, 'playing-event'); | |
| } | |
| function onPause() { | |
| if (manualGraceActive()) { | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'manual-pause'); | |
| return; | |
| } | |
| if (STATE.desiredPlaying) { | |
| scheduleRetry('pause', true); | |
| } | |
| } | |
| function onEnded() { | |
| setDesiredPlaying(false, 'ended'); | |
| } | |
| function onVideoSourceChange() { | |
| setTimeout(() => { | |
| syncVideo(); | |
| if (STATE.desiredPlaying) scheduleRetry('source-change', true); | |
| }, 0); | |
| } | |
| function onNeedRetry() { | |
| if (STATE.desiredPlaying) scheduleRetry('buffering'); | |
| } | |
| function onVisibilityChange() { | |
| const hidden = isActuallyHidden(); | |
| if (hidden) { | |
| STATE.hiddenSince = now(); | |
| if (STATE.desiredPlaying) { | |
| maybeStartKeepaliveAudio(); | |
| scheduleRetry('hidden', true); | |
| } | |
| } else { | |
| STATE.hiddenSince = 0; | |
| if (STATE.desiredPlaying) { | |
| stopKeepaliveAudio(); | |
| scheduleRetry('visible', true); | |
| } | |
| } | |
| } | |
| function onPageHide() { | |
| STATE.hiddenSince = now(); | |
| if (STATE.desiredPlaying) scheduleRetry('pagehide', true); | |
| } | |
| function onPageShow() { | |
| STATE.hiddenSince = 0; | |
| if (STATE.desiredPlaying) scheduleRetry('pageshow', true); | |
| } | |
| function onFreeze() { | |
| if (STATE.desiredPlaying) scheduleRetry('freeze', true); | |
| } | |
| function onResume() { | |
| if (STATE.desiredPlaying) scheduleRetry('resume', true); | |
| } | |
| function patchPause() { | |
| const proto = HTMLMediaElement && HTMLMediaElement.prototype; | |
| if (!proto || typeof proto.pause !== 'function') return; | |
| const nativePause = proto.pause; | |
| proto.pause = function patchedPause() { | |
| try { | |
| if ( | |
| this === STATE.currentVideo && | |
| STATE.desiredPlaying && | |
| isActuallyHidden() && | |
| !manualGraceActive() | |
| ) { | |
| log('blocked hidden pause'); | |
| return; | |
| } | |
| } catch (_) {} | |
| return nativePause.apply(this, arguments); | |
| }; | |
| } | |
| function patchHistory() { | |
| const dispatch = () => window.dispatchEvent(new Event('ytbg-locationchange')); | |
| try { | |
| const pushState = history.pushState; | |
| if (typeof pushState === 'function') { | |
| history.pushState = function () { | |
| const ret = pushState.apply(this, arguments); | |
| dispatch(); | |
| return ret; | |
| }; | |
| } | |
| } catch (_) {} | |
| try { | |
| const replaceState = history.replaceState; | |
| if (typeof replaceState === 'function') { | |
| history.replaceState = function () { | |
| const ret = replaceState.apply(this, arguments); | |
| dispatch(); | |
| return ret; | |
| }; | |
| } | |
| } catch (_) {} | |
| window.addEventListener('popstate', dispatch, true); | |
| window.addEventListener('yt-navigate-finish', dispatch, true); | |
| window.addEventListener('yt-page-data-updated', dispatch, true); | |
| window.addEventListener('ytbg-locationchange', () => { | |
| setTimeout(() => { | |
| syncVideo(); | |
| if (STATE.desiredPlaying) scheduleRetry('navigation', true); | |
| }, 0); | |
| }, true); | |
| } | |
| function startObserver() { | |
| if (STATE.observerStarted) return; | |
| const root = document.documentElement; | |
| if (!root) return; | |
| STATE.observerStarted = true; | |
| const mo = new MutationObserver(() => { | |
| syncVideo(); | |
| if (STATE.desiredPlaying && (!STATE.currentVideo || !STATE.currentVideo.isConnected)) { | |
| scheduleRetry('mutation', true); | |
| } | |
| }); | |
| mo.observe(root, { childList: true, subtree: true }); | |
| syncVideo(); | |
| log('observer started'); | |
| } | |
| function startFrameLoop() { | |
| if (STATE.frameLoopStarted) return; | |
| STATE.frameLoopStarted = true; | |
| const tick = () => { | |
| if (STATE.desiredPlaying) { | |
| if (!STATE.currentVideo || !STATE.currentVideo.isConnected) { | |
| syncVideo(); | |
| } | |
| if (STATE.currentVideo) { | |
| if (isActuallyHidden()) { | |
| if (STATE.currentVideo.paused) scheduleRetry('frame-hidden'); | |
| } else if (STATE.currentVideo.paused) { | |
| scheduleRetry('frame-visible'); | |
| } | |
| } | |
| } | |
| requestAnimationFrame(tick); | |
| }; | |
| requestAnimationFrame(tick); | |
| } | |
| function installMediaSessionHandlers() { | |
| if (!CFG.mediaSession) return; | |
| try { | |
| if (!('mediaSession' in navigator)) return; | |
| if (typeof navigator.mediaSession.setActionHandler !== 'function') return; | |
| navigator.mediaSession.setActionHandler('play', () => { | |
| setDesiredPlaying(true, 'mediasession-play'); | |
| }); | |
| navigator.mediaSession.setActionHandler('pause', () => { | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'mediasession-pause'); | |
| }); | |
| navigator.mediaSession.setActionHandler('stop', () => { | |
| STATE.lastManualPauseAt = now(); | |
| setDesiredPlaying(false, 'mediasession-stop'); | |
| }); | |
| } catch (_) {} | |
| } | |
| function installGlobalListeners() { | |
| document.addEventListener('pointerdown', onPointerLike, true); | |
| document.addEventListener('pointerup', onPointerLike, true); | |
| document.addEventListener('click', onPointerLike, true); | |
| document.addEventListener('touchstart', onPointerLike, true); | |
| document.addEventListener('mousedown', onPointerLike, true); | |
| document.addEventListener('keydown', onKeydown, true); | |
| document.addEventListener('visibilitychange', onVisibilityChange, true); | |
| window.addEventListener('pagehide', onPageHide, true); | |
| window.addEventListener('pageshow', onPageShow, true); | |
| window.addEventListener('freeze', onFreeze, true); | |
| window.addEventListener('resume', onResume, true); | |
| window.addEventListener('focus', onVisibilityChange, true); | |
| window.addEventListener('blur', onVisibilityChange, true); | |
| } | |
| function boot() { | |
| if (STATE.booted) return; | |
| STATE.booted = true; | |
| installVisibilitySpoof(); | |
| patchPause(); | |
| patchHistory(); | |
| installGlobalListeners(); | |
| installMediaSessionHandlers(); | |
| const tryStart = () => { | |
| startObserver(); | |
| if (document.documentElement) startFrameLoop(); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', tryStart, { once: true }); | |
| } else { | |
| tryStart(); | |
| } | |
| setInterval(() => { | |
| if (!STATE.desiredPlaying) return; | |
| syncVideo(); | |
| if (!STATE.currentVideo) return; | |
| if (isActuallyHidden()) { | |
| if (STATE.currentVideo.paused) scheduleRetry('watchdog-hidden'); | |
| maybeStartKeepaliveAudio(); | |
| return; | |
| } | |
| if (STATE.currentVideo.paused) { | |
| scheduleRetry('watchdog-visible'); | |
| } else { | |
| STATE.failures = 0; | |
| resetRetryDelay(); | |
| stopKeepaliveAudio(); | |
| } | |
| }, 250); | |
| } | |
| boot(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment