ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

网页视频进度条无法拖动:倍速与currentTime绕过seek限制

网页视频进度条无法拖动:倍速与currentTime绕过seek限制 上周帮同事看一个在线课程的播放页视频进度条死活拖不动鼠标松开的一瞬间滑块就像被弹簧拉回去一样弹回原位试了三台电脑、两个浏览器都是同样的表现。他一度以为是网速问题或者浏览器坏了准备重装系统。我打开开发者工具看了两眼就笑了——video元素一直在那儿老老实实待着currentTime也正常只是每次赋值之后不到 200 毫秒就被前端脚本改了回来。这就是典型的网页视频进度条无法拖动跟网络、浏览器都没关系是播放器层面主动把 seek 操作给吃掉了。这类场景其实非常普遍在线课程的章节视频、企业内部培训页面、某些自研的播放站点甚至一些文档站的嵌入视频都可能出现进度条锁定。解决的思路有两条一条是加速把播放速率提上去用时间换时间你不需要拖动让视频自己飞快地跑完另一条是跳过也就是绕过前端的拦截直接给播放器的媒体时钟赋值把它送到你想去的位置。下面我把这两条路从原理到实操完整拆一遍包括我自己踩过的坑和几段能直接复制粘贴的控制台代码。1. 进度条拖不动的四类真实成因与快速定位方法在动手改任何东西之前先花两分钟判断属于哪一类问题。这一步看起来多余实际上决定了后面用哪种方案——用错了方案你会一直在原地打转改了半天发现根本方向不对。常见的成因基本可以归到下面四类每一类的表现和定位手段都不一样。1.1 分段加载型滑块弹回是因为目标位置压根没有数据现在绝大多数在线视频都不是一整个 MP4 文件直接喂给浏览器的而是切割成一堆几秒钟的小分片按需拉取再拼起来播放。进度条画出来的是整体时长但浏览器手里真正握着的只有已经缓冲的那一小段。你把滑块拖到很后面播放器就得发起新的分片请求如果服务器响应慢、签名参数过期或者限流请求失败播放器为了保证播放不中断就干脆把你送回去。判断方法很直接打开开发者工具的 Network 面板过滤m3u8、ts、m4s、mpd这些关键字然后尝试拖动进度条看是否有新的请求发出、状态码是不是 200。如果看到一片 403 或者请求根本没发出来那基本就是这一类。这种情况下进度条不是被锁死而是够不着。1.2 前端监听型seeking 事件被拦截后强行回写这是最让人火大的一类。播放器在初始化的时候挂了一个seeking监听或者用定时器盯着timeupdate一旦检测到时间跳跃超过某个阈值立刻把currentTime写回原来的值。你看到的弹回就是这场拉锯战的结果——你赋值一次它纠正一次。定位这段逻辑只要一行代码在 Console 里执行const v document.querySelector(video); v.addEventListener(seeking, e { console.log(seeking 触发目标时间:, e.target.currentTime); });然后去拖进度条。如果日志里看到 seeking 触发了但时间很快又变回去说明确实有人在跟你抢方向盘。更狠一点的做法是监听seeked事件并打印当时的currentTime能看到被纠正后的最终落点。1.3 上报绑定型拖动被当作异常行为直接失效不少学习类站点把观看进度跟账号体系绑在一起。它们的逻辑是前端每隔十几秒往后台发一次心跳上报当前播放到第几秒服务端校验这条上报合不合理——比如两次上报之间隔了 15 秒播放位置却前进了 10 分钟显然不正常于是判定为作弊这段进度不记录。这类站点的前端往往会做得更绝一点直接把进度条做成不可交互的或者拖完之后立刻回位同时后台把这次观看标记为无效。你拖是能拖但拖完退出重进进度还是原地不动。这种情况下加速方案反而比拖动更靠谱因为加速至少是连续播放的形态心跳上报的时间戳对得上。1.4 三步定位法两分钟内确定该走哪条路上面说了半天落到操作上其实就三步。第一步确认 video 元素存在。在 Console 里执行document.querySelectorAll(video).length返回 0 说明视频在 iframe 里或者用了非标准渲染方式那就得换思路返回 1 或更多继续往下。第二步看 src 的类型。执行document.querySelector(video).src如果是以blob:开头说明是 MediaSource 封装分片加载是常态如果是.mp4结尾那多半是完整文件拖动失败的原因基本就是前端拦截。第三步拖一次看事件。用上一节那行代码监听seeking看是否触发、是否被回写。三步走完你心里就有数了。我一般的经验是blob 加 seeking 被回写是最常见的组合大概占了七成以上。剩下的三成里跨域 iframe 和纯分片加载失败各占一半。2. 不写代码的两条路倍速压缩与跳段策略不是所有人都有心情打开控制台敲代码而且有些场景下代码方案会被清理掉。所以先讲两条不动代码就能用的思路它们的适用面比想象中广很多时候能把问题解决得七七八八。2.1 倍速改变的是媒体时钟本质上绕开了跳这个动作playbackRate这个属性的含义是媒体时钟的推进速率。设成 2意味着现实世界过去 1 秒视频时间轴前进 2 秒。理解这一点非常关键倍速方案之所以能解决问题是因为它根本不需要你跳你只是让时间跑得更快。进度条拖不动的根本矛盾在于你想立刻到达某个位置而倍速把这个问题转化成了你愿意花多久到达矛盾自然消解。一个 90 分钟的视频用 4 倍速跑完只要 22 分钟出头用 8 倍速不到 12 分钟。当然实际能不能开到这个倍数取决于播放器的限制这个后面细说。倍速的入口通常有三个播放器自己的 UI 菜单一般在右下角齿轮或者倍速按钮里、右键菜单有些播放器把播放速度放在这里、以及键盘快捷键。键盘快捷键最常见的是Shift加和也有用。和的还有播放器用[]这个得逐个试。你可以在页面上挨个按一遍看右上角有没有出现倍速提示。2.2 用小步跳转替代拖拽为什么一次跳 30 秒比跳 10 分钟靠谱如果你的目标是跳过片头广告或者某段不感兴趣的内容而不是精确到达某个位置那小步快跑比一次大跳成功率高得多。原因还是回到分片加载播放器对已缓冲区域内的 seek 是瞬时完成的对未缓冲区域则要先拉数据。一次跳 10 分钟意味着要重新定位到很远的分片中间任何一环卡住都会失败而每次跳 30 到 60 秒基本都落在预加载范围内成功率高而且失败了也不明显。具体操作上很多播放器支持方向键右箭头前进 5 秒或 10 秒左箭头后退。按住不放可以连续跳。这比拖拽精准也不会触发那些针对大跨度 seek的拦截逻辑。有些播放器还支持JKL三键分别是后退 10 秒、暂停、前进 10 秒这原本是视频剪辑软件的习惯后来被不少网页播放器抄了过去。2.3 什么情况下倍速会自己降回去倍速也不是万能的我遇到过好几种倍速被重置的情况这里列一下省得你以为代码写错了。第一种是播放器主动限制。有些站点在ratechange事件里做了判断发现速率超过 2 就强制改回 1。这种只能靠持续纠正来对抗。第二种是切换分片导致重新初始化。视频每换一个分片某些播放器会把内部状态重置一遍playbackRate也跟着回到 1。表现就是速度提上去几秒又掉回来如此反复。第三种是切到后台标签页被暂停。浏览器为了省电会对后台标签页做定时器节流很多播放器检测到页面不可见直接暂停播放。这个后面还会细说。第四种是播放器内部的地图或笔记功能打断。一些学习站点在视频播放中弹出互动题视频暂停答完继续时速率被重置。对付这几种情况最省事的办法是挂一个轮询定时器隔一两秒检查一次发现不对就改回来setInterval(() { const v document.querySelector(video); if (v v.playbackRate ! 3) { v.playbackRate 3; } }, 2000);轮询间隔别设太密1 到 2 秒足够。设成 50 毫秒那种除了增加 CPU 开销没有任何好处反而可能被某些站点的反作弊逻辑盯上。3. 控制台实操从定位 video 到精确落到指定时间点如果你能接受打开控制台那这条路的上限比倍速高得多因为它直接操作媒体元素本身绕过了播放器 UI 的所有限制。下面按操作顺序拆解每一步都说明白为什么要这么做。3.1 先确认你抓到的是不是真正的播放器一个页面上往往不止一个video标签。广告位、推荐位、悬浮预览窗甚至一些隐藏的预加载元素都可能带video。你随便抓一个就改很可能改的是个根本没在播的东西。所以我习惯先列一遍[...document.querySelectorAll(video)].map((v, i) ({ index: i, src: (v.currentSrc || v.src || ).slice(0, 80), duration: v.duration, width: v.clientWidth, paused: v.paused }));挑duration最大、width最大的那个通常是主播放器。如果duration显示NaN或者Infinity说明元数据还没加载完等几秒再执行。还有一个更快的办法在 Elements 面板里点中那个video元素然后回到 Console 输入$0直接就能拿到它。这个技巧在元素层级很深、选择器不好写的时候特别有用。3.2 playbackRate 的有效范围与静音陷阱浏览器对playbackRate是有取值范围的不是你想设多少就多少。Chrome 内核的实际范围大概在 0.0625 到 16 之间超出这个范围会被自动夹到边界值。你设 100实际生效的是 16。另一个更容易踩的坑是低速播放会自动静音。把速率设到 0.25 以下部分浏览器认为这种播放没有意义直接把音轨关掉video.muted变成 true。反过来高速播放一般不会自动静音但如果超过 4 倍很多播放器自己会做处理比如跳过音频解码以节省资源。还有个细节是音调问题。默认情况下浏览器会做时间伸缩time-stretch保持音调不变所以 2 倍速听起来只是快不会变成花栗鼠。这个行为由preservesPitch控制v.preservesPitch true; // 标准属性 v.webkitPreservesPitch true; // 老版 Safari v.mozPreservesPitch true; // 老版 Firefox如果你想让它变调有人觉得这样更容易跟上语速把它设成 false 就行。但要注意关闭音调保持能省一点 CPU代价是声音失真严重。3.3 currentTime 的三种赋值方式与各自的坑直接赋值是最常用的const v document.querySelector(video); v.currentTime 1800; // 跳到第 30 分钟它的坑在于如果目标位置没有缓冲数据赋值会触发网络请求请求失败的话播放器可能回退到原位置甚至直接进入缓冲卡死状态。第二种是fastSeek()它只跳到目标位置附近的关键帧不做精确解码速度快、开销小v.fastSeek(1800);但这个 API 的支持情况比较微妙。Firefox 和 Safari 支持得比较好Chrome 虽然也有这个方法但很多版本里它的行为跟直接赋值currentTime没区别。所以实际用的时候我一般优先试fastSeek不行再退回直接赋值。第三种是持续补偿式专门用来对付那些会把时间写回原位的播放器(function () { const v document.querySelector(video); const target v.currentTime 600; // 往前 10 分钟 const timer setInterval(() { if (Math.abs(v.currentTime - target) 3) { v.currentTime target; } else { clearInterval(timer); } }, 500); })();这段逻辑的意思是每半秒检查一次只要偏离目标超过 3 秒就重新赋值直到稳定落在目标附近再停止。500 毫秒这个间隔是权衡后的结果——太密了容易被识别成异常行为太疏了又纠正不过来。实测下来这个参数对付大多数回写型播放器都够用。3.4 iframe 场景能做什么和不能做什么如果视频嵌在 iframe 里操作就分两种情况了。同源的情况下还能抢救一下直接穿透进文档const video document .querySelector(iframe) .contentDocument .querySelector(video); video.playbackRate 2;跨域的话contentDocument会返回 null浏览器出于安全考虑不让你碰。这时候控制台方案就彻底失效了只能靠浏览器扩展或者用户脚本管理器让脚本以all_frames的方式注入到子框架里。这个后面第六节会讲。判断是否同源有个快速办法在 Console 里执行上面那段代码如果报的是跨域安全错误那就没戏如果返回 undefined 但没报错说明视频可能还没加载出来多试几次。4. 不同播放器内核的差异怎么分辨各自怎么下手同样是网页视频底层可能是五六种不同的播放器它们的控制入口、对currentTime的处理方式、对 seek 的拦截强度都不一样。用同一套代码去对付所有播放器效果会差很多。先花三秒钟判断类型再选对应的打法。4.1 三秒判断播放器类型查全局变量最快在 Console 里执行这么一行console.log({ videojs: !!window.videojs, hls: !!window.Hls, dashjs: !!window.dashjs, plyr: !!window.Plyr, flvjs: !!window.flvjs });返回true的就说明页面用了对应的库。这比去看 DOM 结构快得多因为不管播放器皮肤怎么改这些库一般都会往 window 上挂东西。如果全是 false那多半是站点自研的播放器这种通常拦截最狠因为它可以完全自己控制媒体元素的行为。4.2 常见内核的控制入口对照判断依据播放器类型推荐控制方式特别注意window.videojs为 truevideo.js优先用videojs.getPlayers()拿实例也可直接操作 DOM实例 API 改速率不会被 UI 覆盖window.Hls为 truehls.js只能操作 media 元素本身跳转未缓冲区域会触发分片请求window.dashjs为 truedash.js操作 media 元素必要时调用seek()时间轴可能有偏移量window.Plyr为 truePlyrplayer.speed 2或直接改 DOM内部会同步 UI 显示全部为 falsevideo 的 src 是 blob自研 MSE 播放器持续补偿式赋值拦截最强需要定时器对抗DOM 里有 iframe 且跨域任意脚本注入或扩展控制台无法直接访问table 里那一列特别注意都是我踩过坑之后加的。比如 dash.js 的时间轴偏移是因为有些直播流的currentTime是从一个很大的基准值开始算的你按 0 到 duration 的直觉去赋值位置会完全不对。这时候得先读一次v.currentTime和v.duration看看数值范围再说。4.3 blob 与 MediaSource为什么跳了又回去在这是常态video.src以blob:开头说明播放器在用 MediaSource 扩展把分片数据喂给媒体元素。这种架构下currentTime的语义发生了变化它指的是整个虚拟时间轴上的位置而浏览器手里的实际数据只有一小段。你赋值到很远的位置播放器必须先把那一段的分片拉回来SourceBuffer才有对应数据可以解码。如果分片请求因为鉴权失败、跨域、限流被拒SourceBuffer拿不到数据播放器就会把时间轴退回上一个有效位置。表现出来就是跳了又回去而且回去的位置往往还挺合理让人以为是前端脚本在捣乱。区分这两种回退有个小技巧看 Network 面板。如果跳转的瞬间有新的分片请求发出并且失败了那是数据问题如果没有任何网络请求时间就被改回去了那是脚本问题。这个判断五分钟就能做完但能省下大量瞎改代码的时间。4.4 关于绕过限制这件事的边界这里得说一句实在话上面这些技巧请只用在你本身就有权限观看的内容上比如自己买过的课程、公司内部的培训视频、公开的讲座录像。绕过访问控制去看本来没权限看的东西不在讨论范围内也容易给自己惹麻烦。我自己的做法是只在那些内容我能看但播放器体验做得烂的站点点用这些技巧。本质上这是一个体验优化问题不是一个权限问题。这条线得划清楚。5. 跑通之后的坑音画、上报、刷新与时间轴错乱代码跑通了只是第一步真正让人抓狂的是后面这些零零碎碎的问题。我把遇到过的情况整理成几类附上排查顺序。5.1 声音变调、自动静音与音频解码压力前面说过速率低于 0.25 会自动静音。但还有一个不那么明显的情况速率开到 8 倍以上时部分设备的音频解码跟不上会出现声音断断续续或者干脆不出声。这时候可以主动把音轨关掉v.muted true;反正 8 倍速下听清内容也不现实静音反而让画面更流畅。如果是语言类的视频我一般把速率控制在 2.5 到 3 倍之间这个区间既能听清又能省下大半时间。还有一点是关于preservesPitch的性能影响。开启时间伸缩意味着浏览器要做额外的音频处理低配设备上可能拖慢整体播放。如果你发现在高速播放时画面卡顿试着把它关掉会流畅不少。5.2 加速了但学习时长一点没涨这是学习类站点最常见的坑。你倍速看了半小时进度条上也显示看完了但退出重进发现记录还在起点。原因就是前面提到的心跳上报机制。这类站点的前端会以固定间隔常见是 10 到 30 秒向后台发送一条记录内容是当前播放到第 N 秒本次心跳时间戳是 T。服务端拿到这条记录之后会跟前一条比对如果时间差是 15 秒播放位置前进了 15 秒正常如果前进了 300 秒直接判无效。所以对付这类站点高速播放是没用的反而容易触发风控。比较稳妥的做法是把倍速控制在 2 倍以内这个区间内时间差和位置差的比例关系还在合理范围内一般不会被拦。或者干脆用分段观看的方式——看一段、暂停、过一会儿继续让上报节奏看起来自然。我实测过一个平台1.5 倍速稳定记录2 倍速十次里有三四次不记录3 倍速基本全丢。这个阈值每个平台不一样得自己试但规律是共通的倍数越高越危险。5.3 页面刷新和路由切换后设置全没了单页应用SPA的站点切换章节不会刷新页面但播放器会销毁重建你之前设的playbackRate和事件监听全跟着没了。表现就是你设好 3 倍速看完一章下一章又变回 1 倍。解决办法是在控制台挂一个 DOM 监听const observer new MutationObserver(() { const v document.querySelector(video); if (v v.playbackRate ! 2) { v.playbackRate 2; } }); observer.observe(document.body, { childList: true, subtree: true });这个监听会在页面结构发生变化时触发覆盖到播放器重建的情况。注意subtree: true是必须的否则只能监听到 body 的直接子节点变化触发不了。如果连页面本身都会刷新比如每章是独立 URL那就只能用用户脚本了这个下一节讲。5.4 时间轴错乱与卡顿的排查顺序遇到跳完之后画面卡住不动进度条显示的位置和实际播放的对不上这类问题按下面这个顺序查基本都能定位到先暂停再跳。高速播放状态下赋值currentTime有时会和正在进行的解码流程打架先v.pause()再赋值成功率高很多。关掉倍速再跳。倍速本身会干扰 seek 的精度先把playbackRate设回 1跳完再调上去。看 Network 有没有 403 或超时。分片请求被拒是最常见的技术原因。清理已经注册的定时器。控制台反复执行代码会叠加多个setInterval它们互相打架表现就是时间反复横跳。用for (let i 1; i 9999; i) clearInterval(i)一次性清掉。刷新页面重来。播放器内部状态被搞乱了之后最省事的办法就是重载。5.5 后台标签页定时器被节流浏览器对不可见的标签页会做定时器节流后台页面里的setInterval最短间隔会被拉到 1 秒甚至 1 分钟。这意味着你的轮询纠正脚本在后台基本失效倍速掉回 1 也纠正不过来。表现就是你切到别的标签页干活回来发现视频停了或者速度掉了。应对方式有两种一是干脆把视频放在前台二是用画中画模式——画中画窗口通常会被当作可见状态不会触发节流。6. 把常用操作固化书签小工具与用户脚本每次都要打开控制台敲代码太累了而且有些站点在打开控制台时会触发调试检测虽然少见但确实存在。把常用操作固化下来是长期使用这类技巧的必修课。6.1 书签小工具最轻量的方案书签小工具就是一段存成书签的 JavaScript点击书签就等于在页面上执行这段代码。它不需要装任何扩展任何浏览器都能用。创建一个新书签名称随便写比如倍速2xURL 那一栏填javascript:(function(){document.querySelectorAll(video).forEach(function(v){v.playbackRate2;});})();保存之后在视频页面点一下这个书签所有视频元素都会被设成 2 倍速。注意开头的javascript:不能少中间整段不能有换行否则部分浏览器会不认。6.2 带参数的可配置版本固定倍速不够灵活可以做成弹窗输入的形式javascript:(function(){ var rparseFloat(prompt(输入倍速例如 2、3、4,2)); if(!r||r0)return; document.querySelectorAll(video).forEach(function(v){ v.playbackRater; v.preservesPitchtrue; }); console.log(已设置为 r 倍速); })();这个版本每次点击都会问你想要多少倍速适应性更强。parseFloat和输入校验是为了防止手滑输入非法值导致脚本报错。6.3 用户脚本处理跨域 iframe 和页面刷新的正解书签小工具在两种场景下会失效视频在跨域 iframe 里以及页面每次切章节都刷新。这两种情况都得上用户脚本管理器比如 Tampermonkey、Violentmonkey 这类因为它们可以在页面加载的早期就注入代码并且支持注入到子框架。一个最小可用的用户脚本长这样// UserScript // name 视频播放助手 // namespace local.video.helper // version 1.0 // description 为网页视频提供自定义倍速 // match *://*/* // run-at document-idle // grant none // /UserScript (function () { use strict; const RATE 2.0; function apply(rate) { document.querySelectorAll(video).forEach(v { if (v.playbackRate ! rate) v.playbackRate rate; }); } setInterval(() apply(RATE), 2000); })();match那一行决定了脚本在哪些站点生效。写成*://*/*是全部站点实际用的时候建议收紧到你常用的那几个域名减少不必要的运行开销也避免在别的地方误伤。run-at document-idle表示在 DOM 基本就绪之后注入这个时机比较稳妥。如果发现脚本执行时视频元素还没渲染出来可以改成document-end试试。6.4 脚本失效的常见原因与维护建议用户脚本用久了会出现以前好好的现在不管用了的情况通常有几个原因站点改了播放器实现。换了个新的播放器库video元素的层级或者创建时机变了。选择器不再匹配。原来用document.querySelector(video)就能拿到现在页面上多了一堆广告视频抓到的是错的那个。站点加了检测。极少数站点会检测playbackRate是否被非 UI 途径修改发现异常就恢复原始值并中断播放。排查的时候先打开控制台看有没有脚本报错再确认document.querySelectorAll(video).length的数量对不对。如果数量对但脚本没效果多半是被前端拦截了那就回到第三节的持续补偿方案双管齐下。我个人维护的脚本里会加一个很小的日志开关默认关掉需要排查的时候打开打印每次设置速率的时间和当时的currentTime。这个日志在判断是脚本没跑还是被覆盖了的时候特别有用比猜快得多。我在长期使用这套方案的过程中最大的感受是不要追求一步到位。很多人上来就想一次跳到视频末尾结果被拦截得死死的觉得所有技巧都是假的。实际上把目标拆小一点先用倍速跑起来再用小步跳转微调最后用持续补偿处理那些顽固的播放器这套组合拳打下来九成以上的网页视频都能顺畅控制。剩下那一成要么是真的跨域加加密要么是站点做了很彻底的保护那就老老实实按正常速度看毕竟内容本身才是目的跟播放器较劲不值当。
返回列表