// ==UserScript== // @name 抖音直播/视频 后台防暂停(脚本猫版) // @namespace com.workbuddy.douyin.keepplaying // @version 1.0.1 // @description 网页版抖音直播页后台防暂停:切后台/最小化时保持直播播放不被自动暂停。仅在 live.douyin.com 与 www.douyin.com/follow/live 生效。右下角浮标显示状态,扩展菜单可一键开关。 // @author WorkBuddy // @match *://live.douyin.com/* // @match *://www.douyin.com/follow/live* // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @run-at document-start // ==/UserScript== (function () { 'use strict'; var STORAGE_KEY = 'kj_keep_enabled'; var USER_CLICK_COOLDOWN = 3000; // 用户交互后 3 秒内不自动恢复 // ===== 开关状态 ===== var enabledRef = true; function getEnabled() { return GM_getValue(STORAGE_KEY, true) !== false; } function setEnabled(v) { enabledRef = !!v; GM_setValue(STORAGE_KEY, enabledRef); updateIndicator(); } // ===== 右下角浮标:让你一眼看出是否在运行 ===== var indicator = null; function ensureIndicator() { if (indicator) return; indicator = document.createElement('div'); indicator.setAttribute('style', 'position:fixed;left:12px;bottom:12px;z-index:2147483647;' + 'background:rgba(254,44,85,0.55);color:#fff;font:12px/1.4 sans-serif;' + 'padding:6px 10px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.3);' + 'user-select:none;cursor:pointer;'); (document.body || document.documentElement).appendChild(indicator); indicator.title = '点击切换 防暂停 开关'; indicator.addEventListener('click', function () { setEnabled(!getEnabled()); }); updateIndicator(); } function isFullscreen() { return !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement); } function updateIndicator() { if (!indicator) return; // 全屏时隐藏,避免遮挡画面 if (isFullscreen()) { indicator.style.display = 'none'; return; } indicator.style.display = 'block'; if (enabledRef) { indicator.textContent = '抖音防暂停 · 运行中'; indicator.style.background = 'rgba(254,44,85, 0.55)'; } else { indicator.textContent = '抖音防暂停 · 已关闭'; indicator.style.background = 'rgba(120,120,120,0.5)'; } } // 全屏切换时同步隐藏/恢复浮标 ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'].forEach(function (ev) { document.addEventListener(ev, updateIndicator, false); }); // ===== 1) 伪装可见性 API(拦截抖音后台检测) ===== function patchVisibility() { try { Object.defineProperty(document, 'hidden', { configurable: false, get: function () { return false; } }); } catch (e) {} try { Object.defineProperty(document, 'visibilityState', { configurable: false, get: function () { return 'visible'; } }); } catch (e) {} try { Object.defineProperty(document, 'webkitHidden', { configurable: false, get: function () { return false; } }); } catch (e) {} } // ===== 2) 伪装 hasFocus ===== function patchFocus() { try { Object.defineProperty(Document.prototype, 'hasFocus', { configurable: false, value: function () { return true; } }); } catch (e) { try { Object.defineProperty(document, 'hasFocus', { configurable: false, value: function () { return true; } }); } catch (e2) {} } } // ===== 3) 静音循环音频(防 Chrome 冻结后台标签页) ===== function silentAudio() { if (window.__keepAliveDone) return; window.__keepAliveDone = true; try { var sampleRate = 8000; var numSamples = sampleRate / 2; var dataSize = numSamples; var buf = new ArrayBuffer(44 + dataSize); var v = new DataView(buf); v.setUint8(0, 0x52); v.setUint8(1, 0x49); v.setUint8(2, 0x46); v.setUint8(3, 0x46); v.setUint32(4, 36 + dataSize, true); v.setUint8(8, 0x57); v.setUint8(9, 0x41); v.setUint8(10, 0x56); v.setUint8(11, 0x45); v.setUint8(12, 0x66); v.setUint8(13, 0x6d); v.setUint8(14, 0x74); v.setUint8(15, 0x20); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, sampleRate, true); v.setUint32(28, sampleRate, true); v.setUint16(32, 1, true); v.setUint16(34, 8, true); v.setUint8(36, 0x64); v.setUint8(37, 0x61); v.setUint8(38, 0x74); v.setUint8(39, 0x61); v.setUint32(40, dataSize, true); for (var i = 44; i < buf.byteLength; i++) v.setUint8(i, 0x80); var blob = new Blob([buf], { type: 'audio/wav' }); var url = URL.createObjectURL(blob); var audio = new Audio(url); audio.loop = true; audio.volume = 0; audio.setAttribute('aria-hidden', 'true'); audio.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;'; var appendAndPlay = function () { (document.body || document.documentElement).appendChild(audio); var p = audio.play(); if (p && p.catch) p.catch(function () {}); }; if (document.body) appendAndPlay(); else document.addEventListener('DOMContentLoaded', appendAndPlay, { once: true }); window.__keepAliveAudio = audio; } catch (e) { try { var ac = new (window.AudioContext || window.webkitAudioContext)(); var osc = ac.createOscillator(); var gain = ac.createGain(); gain.gain.value = 0.0001; osc.connect(gain).connect(ac.destination); osc.start(); window.__keepAliveAC = ac; } catch (e2) {} } } // ===== 4) 拦截更广事件监听 ===== var BLOCKED = { visibilitychange: 1, webkitvisibilitychange: 1, pagehide: 1, pageshow: 1, blur: 1, focus: 1, focusin: 1, focusout: 1 }; var nativeAdd = EventTarget.prototype.addEventListener; var nativeRemove = EventTarget.prototype.removeEventListener; var eventsPatched = false; function shouldBlock(type, target) { if (!BLOCKED[type]) return false; return target === document || target === window; } function patchEvents() { if (eventsPatched) return; eventsPatched = true; EventTarget.prototype.addEventListener = function (type, listener, options) { if (shouldBlock(type, this)) return; return nativeAdd.call(this, type, listener, options); }; EventTarget.prototype.removeEventListener = function (type, listener, options) { if (shouldBlock(type, this)) return; return nativeRemove.call(this, type, listener, options); }; } // ===== 5) 自动恢复:只恢复当前激活媒体,且避开用户操作期 ===== function autoResume() { if (window.__keepAliveResumeInstalled) return; window.__keepAliveResumeInstalled = true; var lastActivity = 0; // 任意页面交互时间(用于避让临时操作) var videoClickTime = 0; // 最近一次点击视频/播放器控件的时间 var userPaused = false; // 用户主动暂停(粘滞,直到下次播放) // 记录用户在视频/播放器区域内的点击,用于区分「主动暂停」与「系统后台暂停」 document.addEventListener('mousedown', function (e) { var t = e.target; if (!t || !t.closest) return; if (t.tagName === 'VIDEO' || t.closest('video, [class*="player"], [data-e2e="play-button"]')) { videoClickTime = Date.now(); } }, true); var mark = function () { lastActivity = Date.now(); }; ['keydown', 'wheel', 'touchstart'].forEach(function (ev) { document.addEventListener(ev, mark, true); }); var activeMedia = null; var tryHook = function (el) { if (!el || el.__keepAliveHooked) return; el.__keepAliveHooked = true; el.addEventListener('play', function () { activeMedia = el; userPaused = false; }); el.addEventListener('pause', function () { if (activeMedia !== el) return; // 抖音切走旧视频 → 忽略 if (!enabledRef) return; // 已关闭 → 不恢复 if (Date.now() - videoClickTime < USER_CLICK_COOLDOWN) { userPaused = true; // 用户刚点过播放器 → 视为主动暂停,保持 return; } // 非用户操作的暂停(系统/后台)→ 自动恢复 var p = el.play(); if (p && p.catch) p.catch(function () {}); }); }; var scan = function () { var list = document.querySelectorAll('video, audio'); for (var i = 0; i < list.length; i++) tryHook(list[i]); }; scan(); new MutationObserver(scan).observe(document.documentElement || document, { childList: true, subtree: true }); setInterval(function () { if (!enabledRef || userPaused) return; // 已关闭 或 用户主动暂停 → 不恢复 if (Date.now() - lastActivity < USER_CLICK_COOLDOWN) return; if (!activeMedia) return; if (activeMedia.paused && !activeMedia.ended && activeMedia.readyState >= 2) { var p = activeMedia.play(); if (p && p.catch) p.catch(function () {}); } }, 1000); } // ===== 启动 ===== function apply() { if (!getEnabled()) { enabledRef = false; updateIndicator(); return; } enabledRef = true; patchVisibility(); patchFocus(); patchEvents(); silentAudio(); autoResume(); updateIndicator(); } if (document.body) ensureIndicator(); else document.addEventListener('DOMContentLoaded', function () { ensureIndicator(); }, { once: true }); apply(); // 脚本猫 / 油猴菜单命令:一键切换开关(即时生效,无需刷新) if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('切换 抖音防暂停 开关', function () { setEnabled(!getEnabled()); }); GM_registerMenuCommand('刷新状态显示', function () { ensureIndicator(); updateIndicator(); }); } })();