Sawana Huang Avatar

Sawana Huang

如何用 DOM 控制 bilibili / YouTube 页面里的视频(2026 版)

Sawana Huang - Fri Aug 01 2025

重新整理如何在网页里拿到真正的 HTMLVideoElement,控制播放、暂停、跳转和倍速,并说明什么时候 `querySelector(\"video\")` 就够了,什么时候该换成更稳的做法。

这篇原本只是我在做浏览器插件时记下的一条发现:很多网页播放器最终都会落到一个 video 元素上,只要拿到它,就能直接控制播放状态。

这个思路现在依然成立,但原文讲得太快了。放到 bilibili、YouTube 这种真实页面里,document.querySelector("video") 经常只是第一步,还不一定就是你真正想控制的那个播放器。

所以这次我把文章重写成一条更稳的实践线:

  • 什么时候直接拿 <video> 就够了
  • 什么时候要先筛选出“当前页面真正正在用的那个视频”
  • 什么时候已经不该继续硬控 DOM,而该换成平台自己的 API

先说结论

如果当前页面里已经有一个可访问的 HTMLVideoElement,那么这些操作通常都成立:

  • play()
  • pause()
  • currentTime = 92
  • playbackRate = 1.5
  • volume = 0.5

但有三个前提最好先记住:

  1. document.querySelector("video") 只会返回第一个匹配元素
  2. 真实页面里可能同时存在多个 <video>
  3. 如果视频在 iframe 里,或者页面是后来动态插入播放器,你要多走一步

为什么这件事能成立

MDN 对 HTMLMediaElementHTMLVideoElement 的定义很清楚:网页里的标准视频元素本身就暴露了播放、暂停、跳转、倍速、音量、事件监听这些能力。

换句话说,你想做的“空降到 1 分 32 秒”“自动暂停”“记录观看进度”,本质上往往只是对视频元素的属性和方法做一次编程控制。

最小例子就是这一句:

document.querySelector("video")?.currentTime = 92;

它在很多页面上都能工作,但复杂页面里别急着把它当成最终答案。

querySelector("video") 有用,但别把它理解得太满

document.querySelector("video") 的含义很简单:返回文档中第一个匹配到的 <video> 元素。

这在下面这些场景里通常够用:

  • 页面里只有一个视频播放器
  • 你是在浏览器控制台里做一次临时调试
  • 你只想快速验证“这个站用的是不是 HTML5 视频播放器”

但 bilibili、YouTube 这类站点更常见的情况是:

  • 页面里可能不止一个 <video>
  • 广告、预览、背景视频也可能占掉第一个匹配项
  • 单页应用切路由后,旧节点会被替换
  • 你的脚本执行时,播放器可能还没插入 DOM

所以更稳的思路是:先找出页面里有哪些 video,再挑出你真正想控制的那个。

一组更稳的选择逻辑

如果你只是做自己的学习工具、浏览器脚本或扩展,我更建议先写一个辅助函数:

function pickMainVideo(root: ParentNode = document): HTMLVideoElement | null {
  const videos = Array.from(root.querySelectorAll("video"));

  if (videos.length === 0) return null;

  return (
    videos
      .filter((video) => video.readyState >= 1)
      .sort(
        (a, b) =>
          b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight,
      )[0] ?? videos[0]
  );
}

这段逻辑做了两件事:

  • 优先选择已经加载过元数据的 video
  • 如果页面里有多个视频,优先挑面积更大的那个

这条规则当然也有局限,但已经比“永远拿第一个 video”更接近真实需求。

你可以先这样验证:

const video = pickMainVideo();

console.log(video);
console.log(video?.currentTime);
console.log(video?.duration);

最常用的几种控制方式

只要拿到 HTMLVideoElement,最常用的操作基本就是下面这些。

1. 播放和暂停

const video = pickMainVideo();

await video?.play();
video?.pause();

这里有个细节要注意:play() 返回的是 Promise。如果浏览器认为当前调用不满足自动播放策略,它会直接 reject。

2. 跳到指定时间点

const video = pickMainVideo();

if (video) {
  video.currentTime = 92;
}

这就是“跳到 1 分 32 秒”的核心做法。很多“空降链接”“回到上次观看位置”“点击字幕跳转”最后都会落到这里。

3. 调整倍速和音量

const video = pickMainVideo();

if (video) {
  video.playbackRate = 1.5;
  video.volume = 0.5;
}

做学习工具、刷课插件、速记播放器时,这两个属性通常是最先用到的。

4. 监听播放进度

const video = pickMainVideo();

video?.addEventListener("timeupdate", () => {
  console.log("current time:", video.currentTime);
});

如果你想实现“自动保存看到哪里了”,这一类事件监听比只在按钮点击时读一次时间更可靠。

5. 请求全屏

const video = pickMainVideo();

await video?.requestFullscreen();

这属于元素级全屏,不等于网站自己的“影院模式”或“网页全屏”。不同站点会在原生全屏外再包一层自己的播放器状态。

做“空降链接”时,关键不在链接本身

原文里提到 bilibili 视频空降链接,这个方向本身没问题,只是实现重点需要换个说法。

如果你是自己做一个视频学习工具,所谓“空降链接”通常包含三步:

  1. 从 URL、笔记卡片或按钮里拿到目标秒数
  2. 等视频元数据加载完成
  3. video.currentTime 设到对应位置

一个更接近实战的版本如下:

function jumpTo(video: HTMLVideoElement, seconds: number) {
  if (video.readyState >= 1) {
    video.currentTime = seconds;
    return;
  }

  video.addEventListener(
    "loadedmetadata",
    () => {
      video.currentTime = seconds;
    },
    { once: true },
  );
}

const video = pickMainVideo();

if (video) {
  jumpTo(video, 92);
}

这样写的好处是:播放器还没完全准备好时,也不会因为 duration 或元数据还没到位而让逻辑显得飘。

浏览器扩展里更常踩的两个坑

我自己后来回头看,这篇最值得补的其实是这部分。

1. 你的脚本执行得太早了

现代视频网站很多都是客户端渲染。content script 先跑了,播放器后出现,这时你会以为“怎么选不到 video”。

最简单的补法有两个:

  • 在合适的时机重试
  • MutationObserver 等待播放器节点进入页面

例如:

function waitForVideo(timeout = 10000): Promise<HTMLVideoElement> {
  return new Promise((resolve, reject) => {
    const existing = pickMainVideo();
    if (existing) {
      resolve(existing);
      return;
    }

    const observer = new MutationObserver(() => {
      const next = pickMainVideo();
      if (!next) return;

      observer.disconnect();
      resolve(next);
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    window.setTimeout(() => {
      observer.disconnect();
      reject(new Error("Timed out while waiting for video"));
    }, timeout);
  });
}

2. 你控制的是父页面,但视频其实在 iframe

这在 YouTube embed 场景里很常见。

如果视频播放器在当前文档的 iframe 里,父页面直接 document.querySelector("video") 是拿不到 iframe 内部那个元素的。这个时候你要么在 iframe 自己的上下文里执行脚本,要么直接使用平台提供的 API。

YouTube 有一类场景更适合直接用官方 IFrame API

如果你是在自己的页面里嵌入 YouTube 视频,想长期稳定地控制播放器,那么官方 IFrame Player API 更合适。

原因很直接:

  • 你的页面里看到的其实是一个 <iframe>
  • 你真正想控制的是 iframe 里面的播放器
  • 官方 API 已经提供了 playVideo()pauseVideo()seekTo() 这类控制方法

这类场景下,继续围着 querySelector("video") 转,维护成本通常更高。

我现在会怎么总结这件事

如果你只是想在 bilibili、YouTube 这类网页里研究“播放器时间如何跳转”“视频怎么暂停”“如何记住上次看到哪”,你的第一站依然应该是 HTMLVideoElement

但文章写到这里,我会把经验压成三句话:

  • 先确认页面里有没有你能访问到的 <video>
  • 别默认第一个 video 就是目标播放器
  • 一旦进入 iframe 或平台封装更重的场景,就优先考虑官方 API

这样理解以后,再去做浏览器扩展、学习工具、自动化脚本,心里会稳很多。

参考资料

作者:Sawana Huang
发布时间:2025年8月1日

声明: 本文采用CC BY-NC-SA 4.0许可协议,转载请注明出处。