Anye
Anye
发布于 2026-06-20 / 17 阅读
0
0

PlayGuard,让你的播放器老实点!

本文由 GLM-5.2 依据 https://cnb.cool/Anyexyz/PlayGuard 完成。

前言

你是否也遇到过这种抓狂的瞬间:

开着某个网页播放器听歌、听课、听播客,切到别的窗口敲点东西,或者把标签页随手切到后台——声音突然就没了。等切回来才发现,它早因为「页面失焦」「标签页不可见」而暂停了。

更气人的是,有些站点更狠:鼠标一离开窗口区域就暂停,甚至直接判断 document.hiddendocument.hasFocus(),在脚本里主动掐掉播放。

对于「我只想在后台安安静静听个响」的用户来说,这种行为非常反直觉。于是就有了 这个小扩展。

🛡 PlayGuard 是什么?

https://cnb.cool/Anyexyz/PlayGuard

PlayGuard 是一个轻量级 Chrome / Edge 扩展(Manifest V3),干的事就一件:在你明确授权的网站上,降低网页播放器因页面失焦、隐藏或脚本调用而停止播放的概率。

说白了,就是把这些「非自愿暂停」的信号和暂停行为,骗回「我还在前台正常播放」的状态。

✨ 核心特点

  • 双 World 协作:充分利用 MV3 的 MAIN 与隔离两种执行环境,既能拿到 chrome.* 权限,又能改写页面原型。

  • 精准拦截:伪造 document.hidden / visibilityState / hasFocus(),并拦下 visibilitychangeblur 等事件。

  • 自动续播:劫持 video.pause(),页面一暂停就偷偷 10ms 后续播,用 WeakMap 防重复。

  • 动态守护MutationObserver 盯着新增的 <video>,点开新一集 / 新一节课也照样保护。

  • :500ms 一轮扫描,不放过任何「应续播却暂停」的视频。

  • 无后台、按需启用:只有你主动加入列表的域名才会被守护,命中次数透明展示。

🏗 它是怎么做到的

整个架构看起来是这样的——脚本分处两个互不相通的执行域,靠 window.postMessage 桥接:

为什么要分两个 world?这是整个方案最妙的地方:

  • page-guard.js 跟页面 JavaScript 共享 DOM、共享原型链。只有在这里才能改写 HTMLMediaElement.prototype.pausedocument.hidden 这些「页面脚本能感知到」的东西。

  • 隔离 world(content.js 拥有 chrome.storage 等扩展权限,但看不到页面的变量。它负责读站点配置、判断当前域名是否启用,再把结果告诉 page-guard.js

  • 弹窗(popup.html / popup.js 管理域名列表、展示状态和命中次数,数据全落在 chrome.storage.local 这个唯一「真相源」上。

各司其职、隔离又协作,这就是它既能拿到 chrome.* 权限、又能改写页面原型的关键。

抢救一条视频的完整流程

当某个已启用站点的视频要被暂停时,PlayGuard 会走这样一条「抢救链」:

核心逻辑都集中在 page-guard.js,拆开看是四件事。

① 伪造页面可见性与焦点

在 MAIN world 里直接重写属性描述符,让页面脚本读到的永远是「我还在前台」:

Object.defineProperty(document, 'hidden', {
  get: () => state.enabled ? false : originalHidden?.get?.call(document),
  configurable: true
});
Object.defineProperty(document, 'visibilityState', {
  get: () => state.enabled ? 'visible' : originalVisibilityState?.get?.call(document),
  configurable: true
});
document.hasFocus = () => state.enabled ? true : originalHasFocus();

同时把 document 上的 visibilitychangewindow 上的 blur 用「捕获阶段 + stopImmediatePropagation」提前拦下,页面脚本根本收不到这些事件:

originalAddEventListener.call(document, 'visibilitychange', stopEvent, true);
originalAddEventListener.call(window, 'blur', stopEvent, true);

pause() 并自动续播

更直接的招数是把 HTMLMediaElement.prototype.pause 换掉。页面想暂停一个「正在播放、未结束、无错误」的视频时,不真正暂停,而是 10ms 后偷偷调一次 play() 续上:

HTMLMediaElement.prototype.pause = function (...args) {
  if (state.enabled && this.tagName === 'VIDEO' && shouldResumeVideo(this)) {
    scheduleResume(this, 10);
    return undefined;          // 骗过调用方:暂停「成功」了
  }
  return originalPause.apply(this, args);
};

为了避免重复排队,用 WeakMap 给每个 <video> 记一个定时器,互不干扰、还能随元素回收自动清理。

③ 守护新增的 <video>

网页常在交互后才动态插入 <video>(比如点开某节课、某集剧)。PlayGuard 用 MutationObserver 盯着整个文档,一旦冒出新的 <video>,立刻挂上 play / pause 监听并打上保护标记:

new MutationObserver(mutations => {
  if (!state.enabled) return;
  mutations.forEach(m => m.addedNodes.forEach(node => {
    if (node.nodeName === 'VIDEO') protectVideo(node);
    else if (node.querySelectorAll) node.querySelectorAll('video').forEach(protectVideo);
  }));
}).observe(document.documentElement, { childList: true, subtree: true });

万一页面用了非常规手段让视频停下,还有个每 500ms 跑一轮的 setInterval 兜底:凡是「应续播却处于暂停」的视频,再补一次 scheduleResume,尽量不留死角。

🚀 使用方法

项目没有构建步骤,加载即用,三步搞定:

  1. 打开 chrome://extensions(Edge 是 edge://extensions)。

  2. 右上角开启「开发者模式」。

  3. 点「加载已解压的扩展」,选中 PlayGuard 目录。

logo.png

加载好之后,点扩展栏里的 PlayGuard 图标,把想保护的网站域名加进去就行:

域名规则很宽松:填 music.example.comhttps://music.example.com、甚至带端口的地址都行,扩展会自动归一化(去 www.、忽略协议)。子域名会跟随主域名一起生效,所以加个主域名就全覆盖了。

🎯 适用场景

说说我自己平时用它对付的几种站点:

  • 在线音乐 / 播客站:后台听歌被打断的元凶,加进去就老实了

  • 网课 / 直播回放:边听课边做笔记,切窗口再也不会暂停

  • 电台 / 听书:长时间后台播放,离开页面也不掐

  • 某些「防挂机」的视频站:那种鼠标离开就暂停的,直接拿捏

📊 数据与状态:一个存储,三处共享

PlayGuard 没有后台常驻脚本,所有状态都存在 chrome.storage.local 里:

  • cleanerSites:你启用的域名列表

  • siteCounters:每个域名被「成功守护」的累计次数

content.js 每次进页面都会把当前域名归一化,再用「最长匹配」判断是否启用;命中就自增计数。弹窗和内容脚本都监听 chrome.storage.onChanged,所以你在弹窗里加一个站点,已经打开的页面会实时生效,无需刷新。命中次数也会随着每次访问自动累加,一目了然。

写在最后

PlayGuard 代码量不大,但把一组很干净的浏览器 API 组合起来,解决了一个非常贴近日常体验的小痛点:MV3 双 world、原型劫持、事件捕获拦截、WeakMap 去重、MutationObserver 动态守护、storage 驱动的无后台架构——每一块都不复杂,凑在一起就刚好。

它不会无差别改写所有网站:只有你主动加入列表的域名才会被守护,命中次数也透明地展示在弹窗里,用着安心。

如果你也有「后台听东西却被打断」的困扰,不妨试试,顺手点个 ⭐。


评论