效率工具

用 Vimeo Player.js 统计真实的观看进度

用 Vimeo Player.js 统计真实的观看进度:记录看过的每一秒,拖动进度条跳过的部分不算;达到 25/50/75/90% 时各记录一次里程碑;进度保存在 localStorage,下次打开时显示“从上次位置继续”。统计逻辑写成单独的模块,用 Node 测试。

BINBHO · 图文教程

用 Vimeo Player.js 统计真实的观看进度:记录看过的每一秒,拖动进度条跳过的部分不算;达到 25/50/75/90% 时各记录一次里程碑;进度保存在 localStorage,下次打开时显示“从上次位置继续”。统计逻辑写成单独的模块,用 Node 测试。

先看清成品

统计模块用 node --test 跑了 3 个测试,全部通过。页面本身没能在本机验证:用 Playwright 驱动无头 Chrome 打开页面后,Vimeo 播放器 iframe 里出现了 Cloudflare Turnstile 人机验证(控制台报 [Cloudflare Turnstile] Error: 600010),播放器一直没有就绪,e2e.py 在 30 秒时超时。我没有尝试绕过这个验证。请在普通浏览器里手动打开验证。

注意:Vimeo 各会员等级的价格、存储和上传额度以官方价格页为准,本文不列数字(官方上传指南里有额度表)。只上传和嵌入你有权使用的视频;不要共享会员账号或 access token;不要绕过隐私设置、密码或禁止嵌入的限制。

做完之后你会得到什么?

成果 说明
tracker.js 按整秒去重,计算百分比和里程碑
index.html 播放器、进度显示、断点续看
e2e.py 端到端测试脚本(本机被人机验证拦住了)

需要准备哪些环境?

项目 要求 说明
浏览器 近期版本的 Chrome、Edge、Firefox 或 Safari 页面要通过 http 服务打开,file:// 下 ES module 无法加载
Node.js 18 以上(本机 v22) 测试 tracker.js
视频 允许嵌入的 Vimeo 视频 示例用的是 Blender 的 Big Buck Bunny(1084537)

文件结构

30-vimeo-B-src/
├── package.json
├── tracker.js
├── tracker.test.mjs
├── index.html
└── e2e.py

第 1 步:统计逻辑

tracker.js:

// 观看进度统计:把看过的整秒记进 Set,拖动进度条跳过的部分不算“看过”。
export function createTracker(duration) {
  const seen = new Set();
  return {
    add(seconds) { const s = Math.floor(seconds); if (s >= 0 && s < duration) seen.add(s); },
    percent() { return duration > 0 ? Math.round((seen.size / Math.ceil(duration)) * 100) : 0; },
    watchedSeconds() { return seen.size; },
    milestones(list = [25, 50, 75, 90]) { const p = this.percent(); return list.filter((m) => p >= m); },
    toJSON() { return [...seen]; },
    load(arr) { arr.forEach((s) => this.add(s)); },
  };
}

timeupdate 在播放过程中会反复触发(频率以 Player SDK 文档为准)。用整秒的 Set 来记录,重复观看不会重复计数,跳过的部分也不会被算进去。

第 2 步:测试

tracker.test.mjs:

import test from "node:test";
import assert from "node:assert";
import { createTracker } from "./tracker.js";

test("拖动跳过的部分不计入", () => {
  const t = createTracker(100);
  for (let s = 0; s < 10; s += 0.25) t.add(s);   // 连续看了 0–10 秒
  t.add(80.5);                                   // 直接跳到 80 秒
  assert.strictEqual(t.watchedSeconds(), 11);
  assert.strictEqual(t.percent(), 11);
});
test("重复观看不重复计数,里程碑", () => {
  const t = createTracker(8);
  for (let r = 0; r < 3; r++) for (let s = 0; s < 6; s++) t.add(s);
  assert.strictEqual(t.percent(), 75);
  assert.deepStrictEqual(t.milestones(), [25, 50, 75]);
});
test("越界和恢复", () => {
  const t = createTracker(10); t.add(-1); t.add(10); t.load([1, 2, 2]);
  assert.deepStrictEqual(t.toJSON(), [1, 2]);
});
node --test      # pass 3, fail 0

第 3 步:页面

index.html:

<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Vimeo 观看进度</title>
<style>body{font-family:sans-serif;max-width:720px;margin:24px auto}.ratio{position:relative;padding-top:56.25%}.ratio iframe{position:absolute;inset:0;width:100%;height:100%}</style>
</head>
<body>
  <div class="ratio">
    <iframe id="v" src="https://player.vimeo.com/video/1084537?dnt=1" allow="autoplay; fullscreen; picture-in-picture" title="Big Buck Bunny"></iframe>
  </div>
  <p>已观看 <b id="pct">0</b>%(<span id="sec">0</span> 秒)· 已达成:<span id="ms">-</span></p>
  <button id="resume" hidden>从上次位置继续</button>
<script src="https://player.vimeo.com/api/player.js"></script>
<script type="module">
import { createTracker } from "./tracker.js";
const player = new Vimeo.Player(document.getElementById("v"));
window.__player = player;
const id = await player.getVideoId();
const KEY = "vimeo-progress-" + id;
const duration = await player.getDuration();
const tracker = createTracker(duration);
const saved = JSON.parse(localStorage.getItem(KEY) || "{}");
if (saved.seen) tracker.load(saved.seen);
const sent = new Set(saved.sent || []);

function render() {
  document.getElementById("pct").textContent = tracker.percent();
  document.getElementById("sec").textContent = tracker.watchedSeconds();
  document.getElementById("ms").textContent = [...sent].join("% / ") + (sent.size ? "%" : "-");
}
if (saved.last > 5) {
  const btn = document.getElementById("resume");
  btn.hidden = false;
  btn.onclick = () => { player.setCurrentTime(saved.last); btn.hidden = true; };
}
let lastSave = 0;
player.on("timeupdate", ({ seconds }) => {
  tracker.add(seconds);
  for (const m of tracker.milestones()) if (!sent.has(m)) { sent.add(m); console.log("milestone", m); }
  if (Date.now() - lastSave > 2000) {          // 每 2 秒存一次,避免频繁写入
    lastSave = Date.now();
    localStorage.setItem(KEY, JSON.stringify({ seen: tracker.toJSON(), sent: [...sent], last: seconds }));
  }
  render();
});
player.on("ended", () => console.log("ended"));
window.__tracker = tracker;   // 方便测试
render();
</script>
</body>
</html>

要点:

  • new Vimeo.Player(iframe) 可以直接接管已有的 iframe;getVideoId()、getDuration() 返回 Promise。
  • 地址里的 dnt=1 让播放器不记录会话数据。按 oEmbed 文档的说法,这样也会屏蔽视频的统计数据,你需要自己权衡。
  • 每 2 秒写一次 localStorage,不要每次 timeupdate 都写。
  • 里程碑现在只打印到控制台,要上报统计时,把 console.log 换成你自己的接口。

第 4 步:打开(本机未在真人浏览器中执行)

cd 30-vimeo-B-src
python3 -m http.server 8000
# 浏览器打开 http://localhost:8000/

检查结果:播放 30 秒后显示约 5%(视频时长 597 秒,来自 oEmbed);拖到中间再播放,百分比只增加实际播放的部分;刷新页面后出现“从上次位置继续”按钮。

调试示例

无头浏览器里播放器一直不就绪

e2e.py 第一次运行时,在 wait_for_function("window.__player") 处等了 60 秒后超时。原因是 window.__player 的赋值写在两个 await 之后,播放器没有就绪,就一直执行不到。改成创建播放器后立即赋值,再运行一次,打开控制台日志后看到了真正的原因:

console: [Cloudflare Turnstile] Error: 600010.
console: Connecting to 'data:,' violates the following Content Security Policy directive: "connect-src 'self' https://hagen.challenges.cloudflare.com ...

Vimeo 播放器在这个无头环境里先弹出了人机验证。这属于平台的防护措施,不是代码的问题。所以本文只把统计逻辑做了单元测试,页面部分请在普通浏览器里人工验证。

常见问题

现象 原因 处理
Vimeo is not defined player.js 没加载成功 检查网络,并确认 script 标签写在 module 脚本之前
播放器显示“无法播放” 视频禁止嵌入,或限制了嵌入域名 换一个视频,或者在 Vimeo 后台把你的域名加进允许列表
百分比到不了 100 最后一秒的 timeupdate 不一定会触发 用 90% 作为“看完”
刷新后进度没了 用 file:// 打开或隐私模式 用 http 服务打开

模板规则

  • 统计逻辑和播放器代码分开,先测逻辑。
  • “看过多少”按去重后的秒数计算,不用当前播放位置。
  • 写入存储要节流。
  • 平台的人机验证不要绕过,碰到了就改成人工验证。

项目资源

用途 地址
Player SDK 概览 https://developer.vimeo.com/player/sdk
Player SDK 参考(事件、方法) https://developer.vimeo.com/player/sdk/reference
player.js 源码 https://github.com/vimeo/player.js
嵌入参数(oEmbed 文档) https://developer.vimeo.com/api/oembed/videos

Player.js 提供事件和 Promise 接口,统计逻辑可以完全由你自己决定。把“看过的秒数”和“播放到哪里”分开计算,统计的完成率才准确。