Press a custom hotkey to capture the current video frame. Supports Shadow DOM and cross-origin iframes.
This is a Vibe Coding project. It extracts the powerful video screenshot feature from h5player and works with the global speed extension to fix the memory leak issue caused by h5player on Bilibili. It works well on the sites I frequently use (e.g., Bilibili, Bilibili Live, YouTube, third-party anime streaming sites, etc.). I am a complete beginner, so PRs are welcome.
This is a Tampermonkey userscript with a single job: press a hotkey on any video website and "capture" the current frame of the video.
Why it exists: many people switched to globalSpeed for playback speed to avoid the memory leak that h5player causes on Bilibili, but in doing so they lost h5player's very handy "video screenshot" feature. This script extracts the video screenshot feature from h5player and brings it back as a standalone tool.
How it differs from a normal "screen capture": a screen capture grabs the pixels on the screen, while this script reads the video frame at its original resolution — unaffected by scaling, danmaku overlays, picture-in-picture, etc. What you get is a screenshot at the video's own quality.
S by default; re-record any key combination from the Tampermonkey menucrossorigin so pixels can be read, with graceful fallback on failureThis is the core logic of the script, and it happens in two steps.
As long as the mouse is hovering over a video, that video is captured unconditionally — no matter how large or prominent other videos on the page are.
The script tracks "which video the mouse is currently over" with a three-layer strategy:
The image below shows the script in action:
[Placeholder image: a page playing a video, with the mouse pointer hovering over the center of the video frame. After pressing S, a separate preview window pops up in the top-right corner of the browser, showing the exact same frame as the video]
When the mouse is on an empty area of the page (or has moved off a video), the script runs the selection logic in findBestVideo():
① Visibility hard filter (fails → eliminated)
r.width > 100 && r.height > 50 // width > 100px and height > 50px
r.top < innerHeight && r.bottom > 0 // within viewport vertically
r.left < innerWidth && r.right > 0 // within viewport horizontally
Videos scrolled out of the viewport, or too small, are excluded from scoring.
② Scoring rule (highest score wins)
score = area (width × height) × playback weight (playing × 2)
!paused && readyState > 2) get double the score;③ Fallback
If no video is "visible", the script falls back to the first video that has decoded a frame (videoWidth > 0); if there is still none, it picks the first candidate.
The image below shows the script in action:
[Placeholder image: a page with two videos at once — a larger one on the left that is playing, and a smaller one on the right that is paused. The mouse pointer is over the empty area in the middle. The script automatically captures the larger, playing video on the left and pops up a preview window]
"Where the mouse is" is the user's strongest signal of intent, so it acts as the decisive factor. When the mouse is not over a video, "large area + currently playing" best represents what the user is actually watching — the scoring logic exists to guess which video the user wants to capture when there's no hover signal.
The script hijacks Element.prototype.attachShadow and forces closed mode to behave like open so internal videos can be accessed, while still faking shadowRoot to return null so the site's own logic isn't broken. A WeakMap maintains a host → shadowRoot reverse mapping for O(1) lookups.
The DOM inside a cross-origin iframe can't be accessed directly, so the script relays via postMessage between parent and child pages: when the top-level page has no video, it broadcasts a VIDEO_CAPTURE message to all iframes, and the script instance inside each iframe performs the capture on its own page.
crossorigin="anonymous" to every <video> so drawImage can read pixels;blob: URLs, to avoid breaking MSE players' internal state) so crossorigin takes effect;crossorigin and retries, ensuring the video can at least play normally.S by default. Re-record any combination (e.g. Ctrl+Shift+S) via the Configure hotkey item in the Tampermonkey menu.
The image below shows the hotkey configuration UI:
[Placeholder image: a centered modal on the page, titled "Set Screenshot Hotkey", showing the current hotkey and the hint "Press a new key combination, or click Save", with Cancel and Save buttons at the bottom]
S to take a screenshot.Pressing the hotkey does nothing?
<video> element (it may be hidden inside Shadow DOM).Why show a preview window instead of downloading directly?
The preview window lets you visually confirm the captured frame is correct, and it keeps the original resolution. If the popup is blocked (especially when triggered through the iframe message chain), allow popups for the site in your browser settings.
This project is for personal learning and legitimate use only. Please respect the copyright holders and the terms of service of the websites.
视频截图工具(提取自 h5player)· 按下自定义快捷键截取当前视频画面,支持 Shadow DOM 与跨域 iframe。
本项目为 Vibe Coding 产物,从 h5player 中提取了强大的视频截图功能,配合 global speed 插件使用,以解决 h5player 在 B 站上导致的内存溢出问题。目前已在常用网站上测试通过(如 B 站、B 站直播、YouTube、第三方动漫网站等)。本人为初学者,欢迎提交 PR。
这是一个 Tampermonkey(油猴)用户脚本,功能只有一个:在任意视频网站按下快捷键,把当前视频的画面"截"下来。
它解决的问题:很多人因为 h5player 在 B 站上的内存溢出问题,改用 globalSpeed 实现倍速,却因此失去了 h5player 中非常实用的「视频截图」能力。这个脚本把「视频截图」功能从 h5player 中独立提取出来,补上这个缺口。
它和普通"屏幕截图"的区别:普通截图截的是屏幕上的像素,本脚本直接读取视频帧的原始分辨率,不受画面缩放、弹幕遮挡、画中画等影响,得到的是视频本身清晰度的截图。
S,可在油猴菜单里录制任意组合键crossorigin 保证能读到像素,失败时自动降级恢复这是脚本最核心的逻辑,分两步。
只要鼠标悬停在一个视频上,脚本就无条件优先截取它,页面上的其他视频即使更大、更显眼也不考虑。
脚本用三层策略追踪"鼠标此刻停留在哪个视频":
如下图是脚本效果图
[占位图:一个正在播放的视频页面,鼠标指针悬停在视频画面中央,按下快捷键 S 后,浏览器右上角弹出一个独立的截图预览窗口,里面是与视频当前帧一致的画面]
当鼠标停在页面空白处(或从视频上移开)时,脚本执行 findBestVideo() 的选取逻辑:
① 可见性硬性门槛(不满足直接淘汰)
r.width > 100 && r.height > 50 // 宽 > 100px 且高 > 50px
r.top < innerHeight && r.bottom > 0 // 在视口垂直范围内
r.left < innerWidth && r.right > 0 // 在视口水平范围内
滚出视口、或尺寸过小的视频,直接不参与评分。
② 评分规则(取最高分)
得分 = 面积(宽 × 高) × 播放权重(正在播放 × 2)
!paused && readyState > 2)得分翻倍;③ 兜底
若没有任何"可见"视频,则退回选取第一个已加载出画面的视频(videoWidth > 0);若仍没有,就取第一个候选视频。
如下图是脚本效果图
[占位图:一个页面上同时存在两个视频——左侧较大且正在播放,右侧较小且已暂停;鼠标指针停留在页面中间的空白处。脚本自动截取了左侧较大的、正在播放的那个视频的画面并弹出预览窗口]
「鼠标放在哪里」是用户最强的意图信号,所以作为决定性依据;而当鼠标不在视频上时,"面积大 + 正在播放"最能代表用户当前正在看的内容,评分逻辑就是用来在没有悬停信号时,尽量猜中用户想截的视频。
脚本劫持 Element.prototype.attachShadow,把 closed 模式强制改为 open 以便访问内部视频,同时对外伪装 shadowRoot 仍返回 null,不破坏站点自身逻辑;再用 WeakMap 维护 host → shadowRoot 反向映射,实现 O(1) 查找。
跨域 iframe 的内部 DOM 无法直接访问,脚本通过 postMessage 在父子页面间接力:顶层页面无视频时向所有 iframe 广播 VIDEO_CAPTURE 消息,iframe 内的脚本实例收到消息后在自己的页面里执行截图。
<video> 添加 crossorigin="anonymous",确保 drawImage 能读取像素;blob: 除外,避免破坏 MSE 流媒体播放器内部状态),保证 crossorigin 生效;crossorigin 并重试,保证视频至少还能正常播放。默认 S。通过油猴菜单 Configure hotkey 可重新录制(如 Ctrl+Shift+S)。
如下图是脚本效果图
[占位图:页面上弹出居中的小窗,标题为 "Set Screenshot Hotkey",显示当前快捷键,提示 "Press a new key combination, or click Save",底部有 Cancel 与 Save 两个按钮]
S 即可截图。按下快捷键没反应?
<video> 元素(可能藏在 Shadow DOM 里)。为什么用预览窗口而不是直接下载?
预览窗口能直观确认截取的画面是否正确,且保留原始分辨率。若弹窗被拦截(尤其 iframe 消息链路触发时),建议在浏览器设置中放行该站点的弹窗。
本项目仅用于个人学习与正当用途,请遵守视频版权方与网站的相关规定。