那次排查网页视频下载时,我最初盯着的是一个看起来很合理的线索:页面上的视频能播放,video 元素也有 currentSrc,那就把这个地址保存下来。播放器已经把内容放出来了,地址又是现成的,按直觉看,下载应该只是把播放动作换成保存动作。
但文件落下来以后,问题很快暴露了。它确实能播放,却不是想要的高码率原片,而是一条约 2Mbps 的预览流。真正的原版请求并没有出现在播放器当前地址里,而是在点击页面上的“下载”按钮之后才出现,码率接近 14.5Mbps。前者足以让人误以为事情成功,后者才是这次任务真正要交付的内容。
“能播放”只证明播放器拿到了可播放内容,并不能证明这个地址代表原始文件。
这件事后来让我重新看待网页下载:它不是简单地从页面上找一个链接,而是要还原用户触发下载时发生的请求,并对结果做独立验收。
表面像选择器问题,实际是下载语义变了
当时页面结构也在变化。原先用来定位弹窗的选择器不再覆盖新的弹窗容器,页面上的视频被放在另一种模态窗口里。乍看之下,最直接的修法是把选择器补上,再沿用旧逻辑从视频元素读取地址。
这个判断只解决了“能不能找到弹窗”,没有解决“找到以后拿到的是什么”。后来检查发现,即使成功打开新的窗口,里面的 currentSrc 仍然是预览地址,参数对应的是较低码率版本。真正的高码率地址要等用户点击右上角的下载按钮,网页才会发起新的请求。
所以这里其实有两个问题:对话框选择器变化,和下载逻辑变化。把它们混成一个问题,很容易出现一种假修复:页面不再报找不到元素,测试也能读到一个地址,但保存下来的仍然是预览文件。
如果不把这两个问题拆开,后面会出现更隐蔽的后果。自动播放、重新渲染、打开弹窗等动作都可能提前产生媒体请求,程序会把最先看到的地址当成下载目标。它在本地能打开,任务表面上就显示成功;等到上传、剪辑或交付时,才发现清晰度和体积都不对,返工成本比当场失败更高。
选择器解决的是“找到操作入口”,请求链解决的才是“拿到正确文件”。
最容易误判的捷径,是直接保存 currentSrc
错误捷径很诱人:看到视频元素能播放,就直接保存 currentSrc,再把“文件已经存在”作为成功标志。这个做法在静态页面上偶尔有效,也因此更容易被当成通用方法。但网页播放器经常会把预览、转码、分段播放和原始下载分成不同的资源。播放地址的职责是尽快让画面出现,下载地址的职责才是满足用户保存原片的意图。
当时的实际对比已经说明了差异:预览流大约只有 2Mbps,而点击下载后出现的候选地址对应约 14.5Mbps 的原版。两者都是合法的媒体地址,格式也可能完全一样。只检查扩展名、响应状态或“能不能播放”,都无法把它们区分开。
这里最容易被忽略的是,浏览器不会替我们解释“原片”这个业务词。它只会按照页面当前阶段发出的请求返回资源。程序如果只观察播放器状态,就等于把“当前正在播放什么”误当成“用户希望下载什么”。
我没有继续围绕 currentSrc 加更多选择器或重试,是因为这条信息本身就不承担下载语义。继续修它,只会让一个错误假设变得更复杂。当页面有明确的下载按钮时,下载按钮就是比播放元素更可靠的行为入口。
把点击动作变成捕获链的起点
修正思路是先改变观察顺序。程序先打开目标内容并定位真实的下载按钮,但不急着把此前看到的媒体地址写入文件。接着在点击前的极短时间内启动请求拦截,只记录这次下载动作产生的候选地址;然后点击按钮,等待请求出现,最后从候选中筛选媒体资源。
“点击前武装”是关键。页面打开、播放器自动播放或弹窗重新渲染时产生的请求,应该被视为背景噪声,不能混入本次下载候选。如果拦截器从页面加载开始就一直记录,最先进入数组的往往正是预览流。只有在 armed 状态下才收集请求,才能把观察范围限制在用户真正触发下载的时间窗口内。
候选地址也不能只按域名或文件后缀筛选。视频请求要检查它是否带有明显的低码率预览参数,并结合码率门槛拒收可疑候选。音频和视频的规则不能简单照搬,因为音频地址携带某些码率参数并不自动意味着它是预览流。这个区别很重要:过于粗糙的规则可能挡住原版音频,过于宽松的规则又会放过低清视频。
我选择短生命周期拦截,是因为它同时保留了两件事:一方面,下载动作的请求不会被错过;另一方面,页面其他阶段的请求不会长期污染候选集合。捕获链的边界应该和用户动作的边界重合,而不是从页面打开后一直漫无目的地监听。
下载完成不等于验收完成
拿到候选地址后,还要经过一次独立的结果验收。首先检查容器和媒体信息,确认文件确实可解析;其次看整体码率、分辨率、时长等属性是否达到任务要求;最后再结合文件大小判断它是否明显偏小。码率不是唯一依据,但它能有效挡住“预览文件冒充原片”这种典型错误。
当时的经验是,对达到一定分辨率的视频设置一个保守的码率地板:如果画面尺寸已经不低,却只有很低的整体码率,就先按预览流处理,宁可失败,也不把它当原片落盘。这个门槛不是对所有视频一刀切的画质标准,而是针对本次已知预览与原版差异建立的安全闸。不同站点、不同编码方式,阈值仍然需要用实际媒体信息校准。
如果候选只有预览流,正确结果应该是明确报错并保留证据,而不是悄悄保存一个“看起来成功”的文件。证据可以包括捕获到的候选类型、媒体信息和失败原因。这样下次调整页面流程时,能判断是按钮没有触发、请求没有被捕获,还是捕获到了但规格不合格。
还有一种边缘情况:点击下载后,浏览器可能把文件交给默认下载目录,而不是直接返回给拦截器。这时也不能因为目标目录没有新文件就放弃,更不能无条件把目录里最近的文件搬走。应当只接管本次动作产生的唯一新增候选,并再次检查容器、码率和时长,确认合格后再归位;如果有多个候选或规格不明,就停在失败状态。
这套方法可以复用在哪里
我后来把这次排查压缩成一条可复用流程。先确认用户真正要执行的动作,是播放、预览、下载还是导出;再定位对应按钮或菜单,而不是默认使用页面上的媒体元素。点击前开启短时拦截,点击后只收集这次动作产生的候选。候选需要按媒体类型和规格筛选,下载完成后再用媒体元数据、大小和时长验收。任何一步不满足,都保留证据并失败关闭。
这套流程不只适用于视频。图片预览图和原图、文档在线预览和导出文件、音频试听和无损下载,都可能共享同一个页面却使用不同请求。页面上看见的资源,是给当前界面服务的;用户点击的动作,才更接近业务意图。
这件事让我重新意识到,自动化最危险的错误不是完全没有结果,而是产生一个足够像正确结果的文件。文件存在、播放器能打开、接口返回成功,这些都只是中间信号,不能代替最终判断。真正可靠的流程,会把“我捕获到了什么”和“我验收通过了什么”分成两件事。
一个下载流程是否可靠,不看它顺利时能否保存文件,而看它遇到预览流时能否拒绝交付。
下次我会怎么做
下次遇到网页媒体下载,我会先问自己:当前看到的地址,是播放链上的资源,还是下载动作产生的资源?如果页面有明确的下载入口,我会把它作为触发点,先武装短生命周期拦截,再执行点击,不再把 currentSrc 当成原片证明。
点击后我会保留候选请求的最小证据,至少能回答它何时出现、属于哪种媒体、规格是否达到门槛。下载落盘后,再检查媒体信息、码率、大小和时长;遇到只有低规格候选、多个不确定文件或目录里无法确认归属的文件,就停止并报告原因。
我也会把页面结构变化和下载语义变化分开排查。弹窗换了容器,说明入口定位需要更新;点击后才出现原片,说明捕获链需要更新。一个流程成熟的标志,不是它永远依赖旧选择器,而是页面改版后仍能从用户动作和结果验收重新建立证据链。
这次真正留下来的,不是某个页面的特例,而是一条工作习惯:先确认动作,再捕获请求,最后验证结果。只要中间任何一步把“能播放”偷换成“是原片”,预览流就会有机会伪装成成功。