想追一档栏目,打开官方直播页,按 F12 翻网络面板,翻了半个小时也翻不出一个能直接塞进播放器的地址,这大概是很多人在做视频采集时遇到的第一个坎。页面上明明播得好好的,可 DevTools 里全是看不懂的接口请求、一堆碎片化的分片,复制下来丢给播放器,要么直接报错,要么几秒钟就断。我最早也是这么折腾过来的,一开始以为是自己抓得不够深,后来才明白:官方直播页面的设计初衷就是不让外部程序拿到一条长期可用的流地址,所以它必然要做地址隐藏、时效签名、请求来源校验这一套。
这篇要聊的东西很具体:用 Python 当胶水,把栏目的直播或点播页面送进 FLVCD(硕鼠)这类在线解析服务,把藏在播放器背后的真实流地址取回来,再交给 FFplay 直接播放、交给 FFmpeg 落成文件。整套逻辑七十行左右,没有复杂的逆向,也不需要读懂播放器的 JS,属于“能跑就行”的实用路线。适合三类人看:一类是想边看边存的普通用户,一类是做媒体内容归档的运营同学,还有一类是刚开始接触流媒体的开发者,想找个能立刻上手的例子理解 m3u8、flv、mp4 这几种形态到底差在哪。
需要先说明的是,这篇讲的都是公开可访问的官方直播页,不涉及任何付费墙内的内容。技术上能拿到地址,和内容上能不能用,是两件事,后文会单独拎出来讲边界。
1. 官方直播页里为什么就是找不到一条能用的 MP4 地址
1.1 页面只是壳子,真正的流在另一层
要理解为什么翻了半天抓不到,得先建立“页面”和“流”是两层东西的概念。你在浏览器里看到的那个播放区域,本质是一个<video>标签或者一个被封装好的播放器组件,它本身并不携带任何视频数据。播放器启动之后,才会去请求一个接口,接口返回一份描述文件或者一个重定向地址,播放器再拿着这个地址去拉数据。这一层一层的,就像你去餐厅吃饭,菜单上写的是菜名,后厨在另一个房间,菜单本身不能吃。
麻烦在于这中间每一层都可能有校验。有的站点在接口层校验 Referer,有的在流地址上带一个几分钟后就失效的签名,还有的会对请求的 User-Agent 做判断。这就是为什么你用浏览器能看,把地址复制到播放器里就 403。不是地址错了,是“身份”不对。所以纯手动抓包这条路,对时效性强的直播流来说几乎不可持续——你今天抓到的地址,明天可能就作废了。
1.2 三种流形态的差别,决定了你后面怎么处理
真正影响后续操作的是返回的流到底是什么形态。这块不搞清楚,后面的播放和下载参数全是瞎调。我按实际使用频率整理了一下:
| 形态 | 典型后缀 | 传输方式 | 播放友好度 | 下载友好度 | 常见场景 |
|---|---|---|---|---|---|
| HLS 分片 | .m3u8+.ts | 短分片循环拉取 | 好,FFplay 原生支持 | 好,FFmpeg 可直接合并 | 绝大多数官方直播 |
| HTTP-FLV | .flv | 单连接长推流 | 好,延迟低 | 一般,中断后文件易损坏 | 部分直播源 |
| 渐进式 MP4 | .mp4 | 单文件分段请求 | 好,可拖动 | 最好,直接存 | 点播、栏目回看 |
HLS 是最常见的。它把一个连续的视频切成若干秒一片的小文件,再配一个索引文件告诉你顺序。好处是适应网络波动,坏处是对下载来说多了一层合并工作。HTTP-FLV 是长连接,一次请求不断吐数据,延迟通常比 HLS 低两三秒,但它没有“索引”这个概念,连接一断,文件末尾就是残的。渐进式 MP4 最舒服,能拖动、能直接保存,可惜直播场景下用得少,多见于栏目回看。
知道了这个分类,你就能判断:如果解析出来的是 m3u8,那就直接交给 FFmpeg,让它自己去拉分片合并;如果是一堆 ts 地址列表,那说明解析站把分片全列出来了,需要自己拼接或者干脆只取索引;如果是单个 mp4,那基本就是点播资源,随便处理。
1.3 解析服务干的事:把壳翻译成流
FLVCD(硕鼠)这类在线解析服务的价值就在这一层。你给它一个栏目页或者直播页的地址,它在服务端模拟一次正常的页面访问,走完播放器那套取流逻辑,然后把最终的真实地址(或者分片列表)吐给你。对你来说,原本要读 JS、要算签名、要拼请求头的工作,被压缩成了一次 HTTP 请求。
这个思路的好处是省事,坏处是依赖外部服务——解析站挂了、改了规则、加了验证码,你的脚本就得跟着改。所以我在写代码的时候会把“解析”和“播放/下载”彻底解耦:解析函数只负责返回一个地址列表,后面所有逻辑都只认地址列表,不关心它是怎么来的。这样将来换成别的解析方式,改动面只有那一个函数。
注意:解析服务返回的地址通常有时效,尤其是直播流,一般几分钟到几十分钟不等。拿到地址后不要存起来第二天用,每次播放前重新解析一次,这是最稳的做法。
2. 整体方案设计:Python 做胶水,FFplay 和 FFmpeg 做引擎
2.1 为什么不自己抓包解密,而选择两段式
理论上你可以自己写一套完整的取流逻辑:模拟浏览器、执行页面脚本、拦截网络请求、提取地址。技术上完全可行,但要付出的代价是维护成本。页面结构一变、加密方式一换,整套逻辑推倒重来。而且很多站点的取流逻辑藏在压缩混淆过的 JS 里,读起来极其痛苦。
两段式的好处是职责清晰。解析这一层交给专门做这件事的服务,它擅长应对各种页面变化;播放和下载这一层交给 FFmpeg 生态,它擅长处理各种流格式。中间用一个简单的 Python 脚本把它们串起来,脚本本身几乎不含业务逻辑,只是“取地址—校验—拼命令—启动进程”。这种结构最大的优势是稳定:不管上游怎么变,你的核心体验不会崩。
还有一个实际考虑:FFplay 和 FFmpeg 是同一套解码库的两个前端,播放能跑通的地址,下载基本也能跑通,参数还可以互相借鉴。如果换成别的播放器,比如某些桌面播放软件,它们在处理直播流的容错上未必有 FFmpeg 这么成熟,遇到分片缺失、时间戳跳变的情况容易直接卡死。
2.2 四个组件的分工,一张表说清
| 组件 | 职责 | 关键能力 | 出问题时看哪里 |
|---|---|---|---|
| Python 脚本 | 调度、拼参数、起进程 | 字符串处理、子进程管理 | 解析结果是否为空 |
| 解析服务 | 页面转真实地址 | 应对页面结构变化 | 返回内容里有没有 http 链接 |
| FFplay | 实时播放 | 低延迟参数、硬解 | 控制台输出的流信息 |
| FFmpeg | 转封装下载 | -c copy免转码、分段录制 | 文件是否能正常拖动 |
这里要特别强调-c copy这个参数的分量。它的意思是“不重新编码,只做封装转换”,直接从源流复制音视频数据到目标容器。带来的好处是 CPU 占用极低,一个普通笔记本同时下三四路都不卡;速度也快,基本等于你的带宽上限。代价是不能改分辨率、不能改码率,源是什么样,存下来就是什么样。对归档用途来说,这恰恰是我们想要的——原样保存,不做二次损失。
2.3 地址时效性带来的设计约束
前面提过地址有时效,但这件事对代码结构的影响比想象中大。如果你的脚本是“解析一次,然后把地址写进配置文件,下次直接读”,那它大概率第二天就失效了。所以正确的做法是每次运行都重新走一遍解析,把地址当成一次性的中间产物,用完就丢。
我在代码里做了一个小设计:解析出来的地址先过一个probe函数,用 FFprobe 快速探一下能不能连上、有没有音视频流。这一步看着多余,实际上能省掉大量排查时间。因为解析站有时候会返回一个“看起来像地址但其实是错误页”的东西,直接丢给 FFplay,你看到的就是一个闪退的黑窗口,什么提示都没有。先探一次,至少能明确告诉你“这个地址是死的”,而不是让你怀疑播放器坏了。
提示:探测这一步会额外花一到两秒,如果你追求极致起播速度,可以做一个开关,调试阶段打开,稳定运行后关掉。
3. 七十行代码拆开看:解析、校验、播放、下载四件事
3.1 解析函数:把栏目页送进解析服务
解析的核心就是构造一个 GET 请求。大多数这类服务的接口形态相似:一个kw参数放原始页面地址,一个format参数选清晰度。原始地址必须做 URL 编码,否则里面的?、&、=会把请求参数搅乱,这是新手最容易踩的坑之一——明明浏览器里能打开,代码里就返回空,八成是没编码。
import re import subprocess import sys from urllib.parse import quote import requests UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36") PARSE_API = "http://www.flvcd.com/parse.php?kw={}&format={}" def parse_page(page_url, quality="high"): """把栏目页地址送进解析服务,返回候选流地址列表""" api = PARSE_API.format(quote(page_url, safe=""), quality) headers = { "User-Agent": UA, "Referer": "http://www.flvcd.com/", } resp = requests.get(api, headers=headers, timeout=15) resp.encoding = "gbk" # 返回页多为 GBK,不解码会乱码 html = resp.text # 优先找索引文件,其次找分片和整文件 patterns = [ r'https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*', r'https?://[^\s"\'<>]+\.mp4[^\s"\'<>]*', r'https?://[^\s"\'<>]+\.flv[^\s"\'<>]*', r'https?://[^\s"\'<>]+\.ts[^\s"\'<>]*', ] for pat in patterns: found = re.findall(pat, html) if found: # 去重但保持原顺序 return list(dict.fromkeys(found)) # 解析失败时把原文片段打出来,方便判断是验证码还是规则变了 print("[!] 未提取到地址,返回内容前 300 字:") print(html[:300]) return []代码不长,但有两个细节值得说。一是resp.encoding = "gbk",很多老牌站点的返回页还是 GBK 编码,不显式指定,requests会按猜测的编码解,中文全是问号,虽然不影响正则匹配 URL,但排查问题时会让你一脸懵。二是提取顺序,先 m3u8 再 mp4 再 flv 再 ts,这个顺序是有讲究的:m3u8 是索引,交给 FFmpeg 最省事;ts 是分片,数量可能有几十上百个,处理起来最麻烦。优先拿索引,是让下游少干活。
另外那个失败时的打印很关键。解析站返回空,可能是页面地址不对、可能是被识别成爬虫要求输验证码、也可能是它的规则没跟上页面改版。打印前 300 字,你一眼就能看出是哪一种。这个习惯我建议保留在所有类似的脚本里——出错时输出“现场”,比输出一句“失败了”有用一百倍。
3.2 地址清洗与探测:先确认这条流是活的
拿到地址列表还不算完,得先确认它是活的。这里用 FFprobe,它是 FFmpeg 套件里的探测工具,专门干“不真正播放、只看流信息”这件事。
def probe(url, referer=None): """用 ffprobe 探测流是否可连,返回 (是否可用, 描述信息)""" cmd = ["ffprobe", "-v", "error", "-user_agent", UA, "-rw_timeout", "8000000"] if referer: cmd += ["-headers", "Referer: {}\r\n".format(referer)] cmd += ["-show_entries", "stream=codec_type,codec_name", "-of", "csv=p=0", url] try: out = subprocess.run(cmd, capture_output=True, text=True, timeout=20) except subprocess.TimeoutExpired: return False, "探测超时" if out.returncode != 0 or not out.stdout.strip(): return False, out.stderr.strip()[:200] or "无有效流" return True, out.stdout.strip().replace("\n", " / ")-rw_timeout这个参数是控制读写超时的,单位是微秒,8000000 就是 8 秒。为什么设这个值?因为直播流在建立连接阶段可能需要几秒握手,设太短会误判成连不上,设太长又会让你干等。8 秒是个折中,实测大部分源都能在这个时间内给出响应。
-user_agent和-headers这两个参数是从“命令行工具”变成“能对付防盗链的工具”的关键。很多流服务器会检查请求来源,不带 Referer 直接 403。这时候你不需要改代码结构,只要把页面地址作为 Referer 传进去就行。这个技巧在下载栏目回看时尤其常用,因为点播资源的校验往往比直播更严。
注意:如果探测返回的是“Invalid data found when processing input”,通常说明拿到的地址其实是个 HTML 错误页,不是媒体流。这时候回头看看解析结果,是不是把页面里的某个跳转链接误当成了流地址。
3.3 播放命令拼装:FFplay 的低延迟参数怎么来的
FFplay 默认参数是给本地文件设计的,直接拿来播直播流,你会看到一个明显的现象:画面能出来,但延迟高得离谱,有时候能拖后十几秒。这不是网络问题,是缓冲策略问题。
def play(url, referer=None, title=None): cmd = ["ffplay", "-fflags", "nobuffer", # 不做输入缓冲 "-flags", "low_delay", # 解码器走低延迟路径 "-probesize", "32768", # 探测数据量压到 32KB "-analyzeduration", "0", # 不等码流分析 "-framedrop", # 解码跟不上就丢帧 "-sync", "ext", # 以外部时钟为基准同步 "-infbuf", # 直播流不限制缓冲队列 "-window_title", title or "live", "-user_agent", UA] if referer: cmd += ["-headers", "Referer: {}\r\n".format(referer)] cmd.append(url) subprocess.run(cmd)逐个说下这几个参数为什么这么设。-probesize默认值在几 MB 级别,意思是“先读这么多数据再判断流格式”,对本地文件无所谓,对直播就是白等。压到 32768 字节(32KB),起播快很多,代价是对某些封装格式特殊的流可能识别不准,这时候可以调回 500000 试试。-analyzeduration单位是微秒,设 0 表示不等分析直接开始,配合前者一起用。
-sync ext是把同步基准设成外部时钟,直播场景下比默认的视频主时钟更稳,不容易出现越播越慢的情况。-framedrop是保命参数,机器性能不够时主动丢帧,保证声音不断——听感上断一下比画面卡死好接受得多。
-infbuf这个参数要单独提一句,它让播放器不限制输入缓冲大小,专门为直播设计。不加的话,播到某个时长可能会因为缓冲区满了而卡住。这个坑我踩过一次,当时以为是源的问题,换了三个地址都一样,最后发现是这个参数没加。
3.4 下载命令拼装:-c copy与分段录制
下载分两种需求:一种是把一整期栏目存下来,一种是长期挂着录直播。前者简单,后者要考虑进程被杀、磁盘写满、文件损坏这些现实问题。
def download(url, out_path, referer=None, duration=None, segment=None): cmd = ["ffmpeg", "-y", "-hide_banner", "-user_agent", UA] if referer: cmd += ["-headers", "Referer: {}\r\n".format(referer)] cmd += ["-i", url, "-c", "copy", # 时间戳归零,避免某些源导致容器报错 "-avoid_negative_ts", "make_zero"] if duration: cmd += ["-t", str(duration)] if segment: # 长时间录制切成小文件,单个文件损坏不影响整体 cmd += ["-f", "segment", "-segment_time", str(segment), "-reset_timestamps", "1", out_path.replace(".mp4", "_%03d.ts")] else: cmd += ["-movflags", "+faststart", out_path] subprocess.run(cmd)先算一笔账,让你对存储有个概念。假设某路直播的码率是 2 Mbps,录一小时的数据量是2 × 3600 ÷ 8 = 900 MB,接近 1 GB。如果码率是 4 Mbps,一小时就是 1.8 GB。这个换算很实用:码率(Mbps) × 时长(秒) ÷ 8 = 体积(MB)。所以录一整晚八小时,按 2 Mbps 算就是 7 GB 左右,磁盘空间得提前看好。
-avoid_negative_ts make_zero这个参数解决的是时间戳问题。有些直播源的时间戳是从一个很大的数值开始的,或者干脆是负的,直接封装进 mp4 会让播放器懵掉,表现为文件能生成但无法拖动、进度条乱跳。加上这个参数,把起始时间戳归零,问题基本消失。
分段录制那个分支是给长时间挂机准备的。-f segment让 FFmpeg 按时间切片输出,-segment_time 600就是每十分钟一个文件。为什么用 ts 而不是 mp4 做切片?因为 ts 是流式容器,写入过程中断了文件依然可读;mp4 的索引信息写在文件头或文件尾,进程被强杀时经常留下一个完全打不开的残废文件。这一点是血的教训,我早期录过一整晚,早上起来发现一个 6 GB 的 mp4 打不开,那种心情不想再体验第二次。
4. 从零跑通一次:环境、代码、实况记录
4.1 环境准备,两个必须检查的点
Python 这边只需要requests,一个命令搞定:pip install requests。FFmpeg 那边麻烦一点,因为它不是一个程序,而是一组程序。你需要确认ffmpeg、ffplay、ffprobe三个都能在命令行直接调用。
Windows 上最常见的坑是“装了但没加进 PATH”。判断方法很简单,打开一个新的命令行窗口,敲ffplay -version,如果能打出版本号就没问题;如果提示“不是内部或外部命令”,说明 PATH 没配好。注意一定要开新窗口,旧窗口不会自动刷新环境变量,这个细节坑过很多人。
Linux 上一般用包管理器装,装完之后同样用ffplay -version验证。有些精简版系统装的是阉割版 FFmpeg,不带 ffplay,这时候需要单独装完整版。macOS 上用包管理器装通常是完整的,不需要额外折腾。
4.2 完整脚本,七十行左右
把前面几块拼起来,加上一个简单的命令行入口,就是完整可用的版本。
def main(): if len(sys.argv) < 2: print("用法: python cctv.py <页面地址> [play|download] [输出文件]") return page = sys.argv[1] mode = sys.argv[2] if len(sys.argv) > 2 else "play" out = sys.argv[3] if len(sys.argv) > 3 else "output.mp4" print("[1/3] 解析页面 ...") urls = parse_page(page, quality="high") if not urls: print("解析失败,检查页面地址或解析服务状态") return print(" 拿到 {} 个候选地址".format(len(urls))) print("[2/3] 探测可用性 ...") picked = None for u in urls[:5]: ok, info = probe(u, referer=page) print(" {} -> {}".format(u[:80], info if ok else "不可用")) if ok: picked = u break if not picked: print("所有候选地址均不可用") return print("[3/3] 开始{} ...".format("播放" if mode == "play" else "下载")) if mode == "play": play(picked, referer=page, title="live") else: download(picked, out, referer=page) if __name__ == "__main__": main()整段代码加上注释大约七十行,核心逻辑就四块:解析、探测、播放、下载。main函数里那个“最多试 5 个候选”的循环是个实用设计——解析站有时会返回一堆分片地址,前面几个可能是广告或占位,往后试几个成功率明显更高。但也不要全试,分片可能有上百个,全试一遍天都亮了。
4.3 运行时屏幕上该看到什么
跑起来之后,正常的输出长这样:先是解析阶段打印候选数量,然后探测阶段逐个打印地址和流信息,类似video,h264 / audio,aac,这说明音视频轨都识别到了。接着 FFplay 会弹出一个窗口,命令行里刷出一串Stream #0:0开头的元信息,包括分辨率、帧率、编码格式。
如果窗口弹出来是黑的,但命令行在刷数据,通常是解码器没匹配上,可以试着加-vcodec指定软件解码。如果命令行直接报Connection refused或403,大概率是请求头没带对,把 Referer 补上再试。如果是Protocol not found,检查一下地址是不是被截断了——有时候从网页复制地址会带上多余的空格或者换行,正则匹配出来是好的,但传给子进程时参数被污染了。
下载模式的输出不一样,你会看到 FFmpeg 打印出frame= 1234 fps= 30 q=-1.0 size= 20480kB time=00:00:41.13 bitrate=4079.2kbits/s speed=1.02x。这里重点看两个数:speed和bitrate。speed接近 1.0 说明下载速度和直播速度同步,这是正常状态;如果长期低于 0.9,说明你的网络跟不上码率,录下来的文件会有缺失。bitrate就是你前面算存储量要用的那个数,可以直接拿这个实测值去估磁盘。
4.4 栏目回看和直播录制,参数要分开调
直播和点播虽然用同一套代码,但参数取向完全相反。直播追求“不停”,所以要开-infbuf、要丢帧、要低延迟;点播追求“完整”,所以不要加任何低延迟参数,让 FFmpeg 按它自己的节奏慢慢拉。
录栏目回看的时候,我一般会把-probesize调回默认,因为点播资源不需要抢那零点几秒的起播时间,反而需要准确识别流结构。如果这期栏目有明确的时长,加-t限制一下更保险,避免拉到片尾的推荐视频里去。另外点播资源通常是渐进式 mp4 或者标准的 HLS,-c copy直接就能存成完好的 mp4,不需要分段。
还有一个小技巧:如果只想存其中一段,比如某期栏目里有个十分钟的访谈想单独留着,可以用-ss指定起点、-t指定长度,配合-c copy做无损截取。注意-ss放在-i前面是快速定位(关键帧对齐),放在后面是精确定位(要解码),两种行为不一样。做无损截取必须放前面,否则 FFmpeg 会重新编码,那就失去-c copy的意义了。
5. 常见问题与排查实录
5.1 一张表先兜住八成情况
| 症状 | 最可能的原因 | 处理办法 |
|---|---|---|
| 解析返回空列表 | 页面地址带了多余参数,或解析站要求验证 | 用干净的页面地址重试,看返回的 300 字提示 |
| 探测显示 403 | 缺少 Referer 或 UA 不对 | 补上页面地址作为 Referer |
| 探测显示 404 | 地址已过期 | 重新解析,不要复用旧地址 |
| 播放几秒后卡死 | 缓冲区策略问题 | 加-infbuf,去掉-rtmp_live类参数 |
| 有画面没声音 | 音频轨识别失败 | 用 ffprobe 看是否有 audio 流,必要时指定-ast |
| 下载文件无法拖动 | 时间戳异常 | 加-avoid_negative_ts make_zero |
| 下载文件完全打不开 | 进程被强杀,索引未写入 | 改用 ts 容器分段录制 |
| 画面撕裂、马赛克 | 带宽不足,丢包 | 降低清晰度档位,或改用有线网络 |
5.2 403 的几种成因,别一上来就怀疑代码
403 是最常见的报错,但它背后至少有四种完全不同的原因,处理方式也不一样。
第一种是请求头缺失。这类流服务器会把“有没有 Referer”当成一道门槛,你只要把页面地址作为 Referer 带上就行。第二种是 UA 被拦,把浏览器 UA 完整复制过去,不要用脚本默认的python-requests/2.x。第三种是地址里带了签名参数,而这个签名有时间戳,过期了就必然 403,这种只能重新解析,没有别的办法。第四种比较隐蔽:地址本身没问题,但服务器限制了并发连接数,你同时开了播放和下载,第二个请求就被拒了。这种情况换一个地址,或者先停掉播放再下载。
排查顺序建议是:先看是不是过期(重新解析一次试试),再看请求头(加 Referer 和 UA),最后看并发。这个顺序是从“最可能”到“最不可能”排的,能省时间。
5.3 卡顿和音画不同步,参数试错的思路
直播卡顿的原因分两类:网络侧的和服务侧的。网络侧的表现是speed长期低于 1,或者 FFplay 里不断打印丢包信息,这时候降低清晰度档位最有效——解析的时候把format从high改成standard或super,码率降下来,稳定性立刻改善。
服务侧的表现比较怪:网络明明很好,speed也正常,但每隔十几秒就顿一下。这多半是分片拉取的节奏问题,可以在 FFmpeg 里加-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5,让它在连接中断时自动重连。这三个参数组合起来的意思是:允许重连、对分片流也生效、最大重连间隔 5 秒。不加的话,一次网络抖动就可能导致整个下载中断。
音画不同步相对少见,但一旦出现很难看。多数情况是源本身的问题,不是你的参数错。可以试着把-sync ext换成默认的视频主时钟,有时候反而更稳。如果还是不同步,那就说明源的音视频时间戳本身是错的,这种情况基本无解,只能换源。
5.4 下载文件损坏的预防比修复重要
前面提过分段录制,这里再强调一次。mp4 的moov原子(存放索引信息的那块数据)默认写在文件末尾,FFmpeg 正常退出时会补写,但进程被 kill、机器断电、磁盘满了,这个补写就不会发生,留下一个体积正常但完全打不开的文件。
预防办法有三个,我一般会同时用前两个。一是输出成 ts 容器,它没有索引依赖,随时中断都能读;二是加-movflags +faststart,这个参数会让 FFmpeg 在写完之后把索引移到文件头,虽然不能解决中断问题,但能让正常完成的文件在网络传输时更快开始播放;三是用-f segment切片,单片损坏不牵连整体。
如果真的已经拿到了一个打不开的 mp4,也不是完全没救。可以试试用 FFmpeg 强制重新封装一遍:ffmpeg -i broken.mp4 -c copy fixed.mp4。如果它能读出部分数据,通常能救回来大部分内容。如果连读都读不了,那就只能找专业的数据恢复工具了,成功率不高。
6. 边界、心得和几条不太正式的建议
6.1 技术上能拿到地址,不等于可以随便用
这一点必须说在前面。这套方法解决的是“怎么把公开可访问的页面里的流交给播放器”这个工程问题,它不改变内容的权属。录制下来的内容如果只是自己看、做笔记、离线补看错过的节目,问题不大;但如果用于二次分发、剪辑搬运、商业用途,那是另一回事,跟技术无关,跟规则有关。
我个人的习惯是:录完的内容只存在本地,不传到任何公开平台,看完一段删一段,不长期囤积。这个习惯没什么技术含量,但它让整件事变得简单——你不需要去纠结边界在哪,因为你压根没走到边界附近。
6.2 几条踩出来的经验
解析这层一定要做成可替换的。我现在的脚本里,parse_page这个函数是独立的,上面还留了一个parse_page_backup的钩子。为什么?因为在线解析服务的不确定性太高,今天好用不代表下周好用,多留一个备选方案,出问题时改一行就能切换,不用动整个流程。
地址别写进代码里。我见过有人把解析出来的地址直接硬编码在脚本里,第二天运行报错,还以为是代码坏了。地址是运行时的中间产物,每次现取现用,这个原则没有例外。
日志一定要留在屏幕上,不要重定向到文件里然后不看。FFmpeg 和 FFplay 的错误信息都很有价值,403、404、Invalid data、Protocol not found这些词直接指向不同的处理路径。把它们藏起来,等于把自己的眼睛蒙上。
最后分享一个我常备的小命令,用来快速判断一个地址到底是活的还是死的:
ffprobe -v error -rw_timeout 5000000 -show_entries format=duration,bit_rate -of default=nw=1 "地址"五秒超时,能出结果就是活地址,报错就是死地址。比打开播放器等黑屏快得多,也比写一堆判断逻辑简单。这个命令我在调试阶段一天要敲几十遍,算是我这套流程里使用频率最高的一个工具。