
IPTV 直播源这东西说穿了就是一行行 URL 加频道名的纯文本。运营商机顶盒里跑的是组播地址网上流传的多半是单播地址格式绕不开 m3u、m3u8、txt 这几种。麻烦在于这些地址会过期、会限速、会换端口同一个源在不同时间、不同网络环境下的表现能差出十万八千里。所以我这几年一直用一套叫 IPTV Checker 的直播源自动检测工具来干这件事几百上千条源一次性丢进去跑一遍把能连上的、响应快的、画质高的挑出来排序剩下的直接扔掉。它不是一个具体软件的名字更像一类工具的统一叫法核心就两个字检测。这套东西适合谁家里折腾过组播转单播、在 NAS 上挂过播放服务、手里攒了一大堆 m3u 却不知道哪条还能用的朋友看完能直接抄作业。纯粹想看直播、不打算动网络配置的朋友也能从里面搞明白为什么我这个地方台卡成 PPT隔壁频道却丝滑的底层原因。下面我把检测这件事从原理到代码、从网络侧配置到踩坑经验完整拆一遍。1. 直播源检测这件事到底难在哪1.1 手工测源的三条死路最开始大家都是手工测。复制一条地址粘到播放器里等它转圈能出画面就留下黑屏就删掉。一百条源这么搞一遍半小时过去了而且测出来的结论基本没用。第一条死路是样本太少。你测的时候那台服务器可能正好空闲等你晚上八点真正想看的时候它已经在给几千个客户端供流首包要等三秒码率掉到 1 Mbps画面糊成一团马赛克。手工测只能反映检测那一瞬间的状态而直播源的质量是随时间剧烈波动的。第二条死路是判断标准模糊。播放器能出画面算通过三秒后卡住算不算通过画面出来了但只有 360p跟标称的 1080p 差了十万八千里算不算通过很多人手里那批源其实有相当一部分是能连上但根本没法看靠眼睛判断根本区分不出来。第三条死路是不能规模化。你攒了八百条源怎么筛一条条试是不可能的。而直播源的生命周期又特别短今天能用的一批两周后可能死掉三成某些频道甚至一天换一次地址。人工维护的成本会迅速超过看直播本身的收益。所以自动检测工具解决的其实是三个问题批量化、量化打分、周期性重跑。这三件事各自都不难难的是把它们串成一条可以无人值守的流水线。1.2 一次合格的检测应该采集哪些指标很多人以为检测就是能不能连上这是最常见的误解。连上只是入场券真正决定观看体验的是后面一串数字。我按重要性排了一下这套指标基本覆盖了 95% 的实际场景。指标含义为什么重要参考阈值TCP 建连耗时三次握手花的时间反映服务器负载和线路质量 500 ms 为优首包时间 TTFB发出请求到收到第一个字节决定换台快不快 1500 ms 可接受采样码率3 秒内收到的字节数换算直接决定画质8 Mbps 以上算高清分辨率视频流实际像素尺寸戳穿标称 1080p 实为 480p1920x1080 及以上编码格式H.264 / H.265 / AVS3影响同码率下的画质与解码压力视播放设备而定连续通过率多次探测的成功比例反映源是否可靠 80% 才值得留HTTP 状态码200 / 302 / 403 / 404区分地址失效和临时抽风只看 200 和正常 302Content-Type返回的内容类型识别伪装成流的 HTML 错误页必须是视频类这份表里最容易被忽略的是最后一行。有些服务器在源失效时不是返回 404而是返回一个 200 的 HTML 页面里面写着资源已过期请重新获取。你的检测脚本如果只看状态码就会把这种源判定为可用然后在播放器里得到一个黑屏还找不到原因。判断依据很简单正常视频流的 Content-Type 一般是video/mp2t、application/vnd.apple.mpegurl、video/mp4这类如果返回text/html无论状态码是多少一律判死。同样容易被忽略的是连续通过率。单次检测有随机性网络抖动、服务器瞬时拥塞都会造成误判。稳妥的做法是同一批源连测三轮间隔几十秒只有三轮都通过的才标记为优质源。实测下来这一招能把误判率压到很低代价只是检测时间乘以三对于后台跑的定时任务来说完全可以接受。2. IPTV Checker 的核心机制拆解2.1 播放列表解析m3u、m3u8、txt 的三种形态所有检测工具的第一道工序都是解析。看起来简单实际上坑不少因为直播源的文本格式根本没有统一标准各家生成器都在自由发挥。标准 m3u 的写法是这样#EXTM3U #EXTINF:-1 tvg-idcctv1 tvg-nameCCTV-1 tvg-logohttp://example.com/logo.png group-title央视,CCTV-1 综合 http://example.com/live/cctv1.m3u8#EXTINF那一行是元数据逗号后面的部分是频道显示名下一行才是真正的播放地址。而实际流传的文件里你可能遇到只有#EXTINF:-1,频道名的简写版也可能遇到连#EXTINF都没有、一行一个 URL 的裸地址版还有些 txt 格式是用频道名,地址或者频道名 空格 地址分隔的。解析的时候我的做法是写一套宽松匹配先按行读遇到#EXTINF就抓tvg-name、group-title这两个关键字段抓不到就退回到取逗号后面的文本遇到http开头的行就当成地址和上一条元数据配对。如果地址行前面没有元数据就用 URL 的最后一段当频道名。这套逻辑写下来不到五十行但能吃掉市面上绝大多数格式变体。还有一个细节地址后面常带查询参数比如?tokenabcexpire1735689600。这些参数是有用的不能为了去重把它们删掉因为不同 token 可能就是不同源。去重应该用域名路径作为键忽略查询串这样既能识别真正的重复又不会误杀。2.2 探测层的三种技术路线与取舍解析完了就是探。这一步有三条主流路线各有适用场景选错了要么慢得离谱要么结论不准。第一条是纯 HTTP 探测。用 HTTP 客户端发一个 GET读几秒的数据统计字节数和耗时。优点是快、并发高、资源占用低一台普通设备跑几百并发毫无压力。缺点是它只能告诉你这条链接在传数据无法确认传的是不是可解码的视频。有些源的返回内容其实是错误提示页字节数看着还行实际一点用没有。第二条是 ffprobe 探测。调用 FFmpeg 套件里的ffprobe让它去解析流然后输出分辨率、编码、码率、帧率等元信息。优点是结论权威能直接拿到分辨率判定标称高清实为标清这种问题特别准。缺点是慢一次探测动辄三五秒而且每个探测都要起一个进程并发上去了 CPU 直接吃满。第三条是混合路线也是我最后采用并推荐的做法先用 HTTP 探测做初筛把明显连不上的、超时的、返回 HTML 的一大批先踢掉剩下大概 20% 到 30% 的候选源再交给 ffprobe 做精检。这样既保证了速度又拿到了准确的分辨率信息。实测一千条源纯 ffprobe 要跑十几分钟混合方案两分钟出头就能出结果而且结论一模一样。注意ffprobe 必须显式设置超时参数否则遇到那种连上但不发数据的源它会一直挂在那里等整个检测流程就卡死了。2.3 评分模型怎么把一堆数字变成一个排序拿到一堆指标之后得有个办法把它们合成一个分数才能排序。我的评分模型是这样设计的权重可以根据自己的偏好调整总分 40 × 响应分 35 × 画质分 25 × 可靠分 响应分 clamp(1 - TTFB / 3000, 0, 1) 画质分 clamp(码率 / 12000000, 0, 1) × 0.6 clamp(宽度 / 1920, 0, 1) × 0.4 可靠分 通过轮次 / 总轮次这套公式背后的逻辑很直白。响应分决定换台体感权重给 40 是因为它最影响第一印象画质分决定观看体验但码率不是越高越好超过 12 Mbps 之后边际收益很低所以做了截断可靠分是长期可用性的保障权重 25 是因为单次检测的可靠分意义有限它的价值在于多轮累积。实际用的时候还有个隐藏技巧同一个频道往往有多个源评分排序之后不要只留第一名留前三名做成备份组。播放器支持多源自动切换的话主源挂了能瞬间切到备源体验会好很多。这个细节后面第 5 章会展开讲。3. 从零实现一个可落地的自动检测脚本3.1 环境准备与依赖清单整个工具链只需要 Python 3.9 以上版本加上几个库。我不推荐用太重的东西检测脚本越轻越好跑在路由器、NAS、甚至老旧笔记本上都行。python3 -m venv venv source venv/bin/activate pip install aiohttp ffmpeg-python系统层面还需要装 FFmpeg因为要用到ffprobe。Debian/Ubuntu 系一条命令搞定容器环境直接用linuxserver/ffmpeg这类镜像做基础层也行。如果你的 NAS 系统自带应用市场通常也能直接装。apt-get update apt-get install -y ffmpeg ffprobe -version选aiohttp而不是requests原因是前者是原生异步能在单线程里跑几百个并发请求内存占用却很低。用requests加线程池也能实现但线程数一多上下文切换开销就上来了五百并发的时候差距非常明显。提示如果检测设备的内存比较紧张把并发数控制在 200 以内。每个并发连接大约占用几十 KB 到几百 KB 的缓冲区取决于你读多少数据。不做限制的话几百并发加上读缓冲能把小内存设备压垮。3.2 列表清洗、归一化与去重第一步是把原始列表洗干净。很多网上流传的 m3u 文件里混着空行、注释、重复项还有格式错乱的条目直接进检测流程会造成大量无效请求。import re from urllib.parse import urlparse, urlunparse EXTINF_RE re.compile(r#EXTINF:.*?,(.*)$) ATTR_RE re.compile(r(\w[\w-]*)([^]*)) def parse_playlist(text): entries [] pending None for raw in text.splitlines(): line raw.strip() if not line: continue if line.startswith(#EXTINF): name EXTINF_RE.search(line) attrs dict(ATTR_RE.findall(line)) pending { name: attrs.get(tvg-name) or (name.group(1).strip() if name else ), group: attrs.get(group-title, 未分组), } continue if line.startswith(#): continue if line.startswith((http://, https://, rtp://, rtsp://)): item dict(pending or {}) item.setdefault(name, line.rstrip(/).split(/)[-1] or 未知频道) item.setdefault(group, 未分组) item[url] line entries.append(item) pending None return entries def dedup_key(url): p urlparse(url) return urlunparse((p.scheme, p.netloc, p.path, , , ))这段代码里有三个值得说的点。第一元数据可能缺字段所以每个字段都要有兜底不然程序会在意料之外的地方崩。第二除了 http/https我还把 rtp 和 rtsp 前缀保留下来了虽然这两类地址不能用 HTTP 方式检测但它们有自己的处理方式直接扔掉太浪费。第三dedup_key只保留协议、主机和路径丢掉查询串这样带不同 token 的同一路径会被识别为重复项。去重之后再按 URL 排序输出一个干净列表。顺手统计一下分组分布你会对这批源的结构有个直观认识比如央视有 80 条、卫视有 220 条、地方台有 400 条后续可以按需要只检测关心的分组能省下大量时间。3.3 并发探测核心代码逐段说明核心探测逻辑用aiohttp写控制在 80 行以内。关键在于超时配置要拆成三段不能用一个总的超时糊过去。import asyncio import time import aiohttp CONNECT_TIMEOUT 3.0 # 建连超时 READ_TIMEOUT 2.0 # 单次读取间隔超时 SAMPLE_SECONDS 3.0 # 采样时长 async def probe(session, sem, entry): url entry[url] result {**entry, ok: False, ttfb: None, bitrate: 0, ctype: , status: 0, reason: } async with sem: start time.perf_counter() try: timeout aiohttp.ClientTimeout( connectCONNECT_TIMEOUT, sock_readREAD_TIMEOUT, totalSAMPLE_SECONDS CONNECT_TIMEOUT 2, ) async with session.get(url, timeouttimeout, allow_redirectsTrue) as resp: result[status] resp.status result[ctype] resp.headers.get(Content-Type, ) total_bytes 0 deadline start SAMPLE_SECONDS async for chunk in resp.content.iter_chunked(64 * 1024): if result[ttfb] is None: result[ttfb] time.perf_counter() - start total_bytes len(chunk) if time.perf_counter() deadline: break elapsed max(time.perf_counter() - start, 0.001) result[bitrate] int(total_bytes * 8 / elapsed) if resp.status not in (200, 206): result[reason] fHTTP {resp.status} elif text/html in result[ctype].lower(): result[reason] 返回网页而非视频流 elif result[ttfb] is None or result[ttfb] 2.5: result[reason] 首包超时 elif total_bytes 65536: result[reason] 数据量不足 else: result[ok] True except asyncio.TimeoutError: result[reason] 超时 except aiohttp.ClientError as e: result[reason] f连接错误 {type(e).__name__} except Exception as e: result[reason] f未知错误 {type(e).__name__} return result async def run_all(entries, concurrency150): sem asyncio.Semaphore(concurrency) connector aiohttp.TCPConnector( limitconcurrency, limit_per_host8, ttl_dns_cache300) async with aiohttp.ClientSession(connectorconnector) as session: tasks [probe(session, sem, e) for e in entries] return await asyncio.gather(*tasks)这份代码有三个地方是踩过坑才改出来的。limit_per_host8这一行非常关键。很多直播源集中托管在少数几个域名下如果不加单主机限制150 个并发全砸到同一个域名对方服务器会直接把你的连接拒掉或者限速检测结果会大面积误判为不可用。限制到 8 之后同一批次里那些同域的源会被排队处理结论反而更真实。TTFB 的测量位置也有讲究。我是在第一个数据块到达时才记录而不是等响应头。原因是有些服务器会立刻返回响应头然后隔两秒才开始发数据如果按响应头算就会把这种慢源误判成快源。ttl_dns_cache300是为了省 DNS 解析时间。一批一千条源里域名可能只有几十个缓存之后能省掉大量重复解析整体速度提升相当明显。3.4 用 ffprobe 补全分辨率与编码信息初筛通过的源进第二道工序。ffprobe的命令行参数我调过很多次下面这版是兼顾速度和信息完整度的版本ffprobe -hide_banner -v error \ -analyzeduration 1000000 -probesize 500000 \ -select_streams v:0 \ -show_entries streamcodec_name,width,height,avg_frame_rate,bit_rate \ -of json \ -rw_timeout 4000000 \ http://example.com/live/channel.m3u8参数含义逐个说清楚。-analyzeduration和-probesize控制探测深度默认值偏保守探测一个流可能要十几秒把它们压到 1 秒和 500 KB 之后绝大多数流在一两秒内就能给出结论。-select_streams v:0只取第一条视频流避免音频流信息干扰。-rw_timeout是读写超时单位微秒四百万就是一秒这个参数必须有否则挂起的流会让整个进程卡死。返回的是 JSON解析之后能拿到width、height、codec_name、avg_frame_rate这些字段。看到 1920x1080 就是真高清看到 640x360 就是标清一眼分明。import json, subprocess def probe_stream(url, timeout6): cmd [ffprobe, -hide_banner, -v, error, -analyzeduration, 1000000, -probesize, 500000, -select_streams, v:0, -show_entries, streamcodec_name,width,height,avg_frame_rate, -of, json, -rw_timeout, 4000000, url] try: r subprocess.run(cmd, capture_outputTrue, textTrue, timeouttimeout) if r.returncode ! 0: return {} info json.loads(r.stdout) streams info.get(streams) or [] return streams[0] if streams else {} except Exception: return {}subprocess.run外面还包了一层timeout这是双重保险。ffprobe 自己的超时参数偶尔会失效进程级超时能兜住极端情况。这两层保护加上之后我跑的几千次检测里再没出现过卡死。3.5 结果落库、排序与定时调度检测完的结果不能只打印到屏幕上一定要落库否则没法做趋势分析和多轮比对。用 SQLite 就够了一个文件零运维。CREATE TABLE IF NOT EXISTS probe_result ( url TEXT NOT NULL, name TEXT, grp TEXT, ts INTEGER NOT NULL, ok INTEGER, ttfb REAL, bitrate INTEGER, width INTEGER, height INTEGER, codec TEXT, reason TEXT, PRIMARY KEY (url, ts) ); CREATE INDEX IF NOT EXISTS idx_url_ts ON probe_result(url, ts DESC);表结构里ts是时间戳和url一起做主键这样同一地址在不同批次的检测结果会分开存方便对比。索引建在(url, ts DESC)上查询某条源最近三次检测结果时速度很快。调度直接用系统自带的定时任务不用额外引入调度框架。我的配置是每天凌晨跑一次全量晚上七点半跑一次重点分组因为那个时间点正好是晚高峰前测出来的结果更有参考价值。30 2 * * * cd /opt/iptv-checker ./venv/bin/python main.py --all logs/daily.log 21 30 19 * * * cd /opt/iptv-checker ./venv/bin/python main.py --group 央视,卫视 logs/peak.log 21输出的时候除了生成新的 m3u 文件我还会同时生成一份精简版只保留每个频道得分最高的三条地址按主备顺序排列。播放器读这份精简版换台和容错都更省心。4. 本地网络侧的配合组播、udpxy 与单线复用4.1 udpxy 与 msd_lite把组播变成 HTTP 单播检测工具本身不解决流从哪来的问题。如果你家里有一路运营商给的组播信号想让它被多个设备同时播放中间需要一个转换环节把组播转成普通的 HTTP 单播流。这就是 udpxy 之类的工具在做的事。原理不难理解。组播是一份数据发给一组接收者局域网上只要有设备加入了那个组播组就能收到。问题是很多播放设备和软件不支持组播无线网络对组播的支持也很差。转换工具做的事情就是自己加入组播组把收到的数据复制一份通过 HTTP 单播发给每一个请求它的客户端。客户端拿到的是一个形如http://192.168.1.10:4022/rtp/239.x.x.x:1234的地址跟普通 HTTP 流没区别。用 udpxy 和用 msd_lite 的区别主要体现在资源占用上。udpxy 功能全、配置项多但多路转发时 CPU 占用偏高msd_lite 更轻量同样路数下 CPU 占用大概只有前者的三分之一到一半代价是配置项少一些。跑在性能有限的设备上我一般优先选后者。配置里最值得注意的参数是缓冲区大小。默认值在个别场景下会造成花屏适当调大能明显改善。另外转换服务本身不消耗流量它只是把已经收到的数据复制分发所以转发路数增加时主要压力在网络和 CPU而不是在接收端。4.2 一路流占多少带宽一台设备能带几台电视这是问得最多的问题我直接给一张换算表。前提说明以下都是估算值实际会受编码效率、网络质量和设备性能影响。场景单路码率可用带宽估算理论上限实际推荐高清频道千兆有线8 Mbps800 Mbps约 100 路30 至 50 路4K 频道千兆有线30 Mbps800 Mbps约 26 路8 至 15 路高清频道5GHz 无线8 Mbps400 Mbps约 50 路10 至 15 路高清频道2.4GHz 无线8 Mbps80 Mbps约 10 路3 至 5 路这张表里最关键的一列是实际推荐。为什么理论上千兆能带 100 路实际只推荐 30 到 50 路因为千兆网口的 940 Mbps 是理想值实际有协议开销、有突发流量、有交换机背板的转发压力更重要的瓶颈通常在 CPU每复制一份流都要消耗一点处理能力路数一多就压不住了。而且家里不可能只有 IPTV 在跑还有下载、备份、视频会议在分带宽。无线那一行尤其要打折扣。2.4GHz 频段本身可用带宽就小再加上组播和单播混跑时的重传开销实际能看的路数往往比表里更少。我的建议是只要超过三台设备同时看就走有线或者至少走 5GHz 频段的独立 SSID。4.3 单线复用与桥接改动的实操要点很多人家里装修时只在一面墙留了一根网线机顶盒和路由器都要接于是就有了单线复用这个需求。核心原理是让一根网线同时承载两个逻辑网络一个是上网用的网络一个是 IPTV 用的网络靠 VLAN 标签把它们区分开。实现方式有两种。一种是路由器支持 IPTV 功能可以在管理界面里指定某个 LAN 口走 IPTV 通道由路由器自己打标签。另一种是中间加一台支持 802.1Q 的交换机两端都配置成 trunk 口一侧接光猫、一侧接墙内网线到房间那头再用另一台交换机解标签分别接机顶盒和路由器。配置要点有三条。第一两个网络的 VLAN ID 必须两侧一致各地运营商用的编号不一样这个必须以本地实际配置为准不能照搬网上的数字。第二网吧和 IPTV 的 VLAN 不能都打成 untagged 接同一个口否则会被识别成同一个网络直接冲突。第三trunk 口上的 PVID 要设置正确设错了会出现IPTV 能看但上网断了或者反过来的情况。至于光猫改桥接之后 IPTV 还能不能用答案是通常可以但要看具体接法。IPTV 业务的 VLAN 在二层是透传的和光猫是不是负责拨号没有直接关系。如果原来是机顶盒接光猫的 IPTV 专用口改成桥接加单线复用之后就得把那个口的 VLAN 标签带到新的链路上机顶盒才能认出自己该加入哪个组播组。改之前建议先拍照记录原始配置改完不通能马上回退。4.4 NAS 部署飞牛与群晖上的容器化选择检测脚本和转换服务都适合放在 NAS 上跑因为 NAS 常年开机、网络稳定、有足够的存储保存历史数据。现在主流的家用 NAS 系统对容器支持都不错部署过程大同小异。用容器部署的好处是环境隔离干净不用在宿主机上装一堆依赖升级和迁移也方便。写一个 compose 文件把检测脚本和转换服务都定义进去用同一个自定义网络剩下的交给系统管理。几个建议把配置文件和历史数据库挂载到宿主机目录上这样容器重建数据不丢给容器设置重启策略设备和系统重启后服务能自动起来注意网络模式的选择组播转发需要能看到局域网的组播流量用桥接网络时通常会失败这种情况需要把容器切到主机网络模式。资源占用方面检测脚本在检测时段会短暂吃一些 CPU跑完就回落日常几乎无感。转换服务在转发多路时的 CPU 占用是持续性的性能较弱的 NAS 建议控制同时转发的路数或者干脆只在需要时启动。硬盘方面如果历史数据保留超过半年SQLite 文件会涨到几百 MB定期清理旧记录就能控制住。5. 常见问题与排查速查5.1 检测全绿却放不出来五个高频原因这是最让人抓狂的情况检测报告一片绿实际播放全是黑屏。按我遇到过的频率排序通常是下面这五个原因。第一个是分辨率探测拿到了错误结论。有些源在开头几秒会推送一段占位画面分辨率很低ffprobe如果只分析前 500 KB拿到的就是这段占位画面的参数。解决办法是加大probesize或者指定跳过前几秒再分析。第二个是并发目标被限速。同一批源里如果大量条目指向同一个域名并发请求会把对方触发限速机制返回一个内容不完整的响应。现象是检测时数据量看着够但播放器一解码就报错。把单主机并发压到个位数基本可以避免。第三个是播放器兼容性问题。检测用的是通用协议解析播放器可能有自己的格式偏好比如只接受某几种封装格式或者对 HLS 分片长度有要求。换一个播放器交叉验证是最快的定位方法。第四个是本地路由问题。有些源走的是特定网络出口你的设备如果被策略路由分流到了另一个出口就会连不上。这种情况下检测脚本和播放器如果在同一台设备上结论应该一致如果检测在 NAS 上、播放在手机上就可能出现差异。排查方法很简单在出问题的那台设备上重新跑一次检测。第五个是频道本身的分片鉴权。部分源在拉取具体分片时会附带额外的校验参数播放列表能拿到但分片请求会被拒。这种源在检测阶段表现为能连上但数据量不足看到这个原因标记就要多留个心眼。5.2 结果每次都不一样波动溯源同一批源连测两次结果对不上这事我遇到过很多次。原因无非三类服务器侧、线路侧、检测侧。服务器侧最常见。热门源的并发用户数随时间剧烈变化晚高峰时段的可用性会明显下降。判断方法是在不同时段各跑一次看同一批源的成功率曲线如果曲线和时段强相关那就是服务器负载问题不是你的检测有问题。线路侧的问题通常来自本地网络。家里的下载任务在跑或者无线信号在波动都会影响结论。把检测设备改成有线连接并且确保检测时段没有大流量任务能排除大部分干扰。检测侧的问题主要出在超时阈值上。阈值卡得太紧网络稍微抖一下就被判超时卡得太松慢源会被误判为好源。我的经验是阈值不要写死而是根据你自己的网络基线来定。做法是先测一批已知可靠的源记录它们的 TTFB 分布取 95 分位数作为阈值参考这样定出来的数字才贴合实际。5.3 报错信息与处理方式对照表报错/现象常见原因处理方式ClientConnectorError域名解析失败或端口不通检查 DNS确认端口是否被封ServerTimeoutError服务器响应过慢提高超时阈值或标记为慢源ServerDisconnectedError服务器主动断开降低并发避免触发限速HTTP 403需要鉴权或来源校验检查地址是否带 tokenHTTP 404地址已失效直接丢弃或标记为待更新返回 text/html伪装成流的错误页一律判死不要犹豫数据量不足分片鉴权或流不稳定加大采样时长复测一次ffprobe 无输出流格式不标准或超时提高 rw_timeout 并加大 probesizeCPU 占用 100%并发过高或 ffprobe 进程过多降低并发分批处理内存持续增长会话未正确关闭确保使用异步上下文管理器这张表我贴在自己的项目 README 里出问题先查表能解决八成以上的故障。6. 一些踩坑之后才明白的细节6.1 检测时间点比检测算法更重要脚本写得再漂亮如果在错误的时间跑结论一样没有参考价值。我最初的版本是凌晨两点跑因为那时候网络空闲、速度快。结果生成的播放列表在晚上八点用起来一半的源都不能看。后来我把主要检测任务挪到了晚高峰前也就是晚上七点半左右。这个时间点服务器已经开始承压但还没到最拥挤的时候测出来的结论更接近真实观看体验。同时保留了凌晨的全量检测用来发现新增的可用源。两套数据分开存用的时候按场景选。还有一个细节不要只测一轮就下结论。同一批源在同一时段连测三轮间隔一分钟只有三轮都通过或者通过率高的才进入优质列表。这个改动看起来简单实际对可用性的提升非常明显。6.2 多源备份与自动切换的价值前面提过每个频道保留三条来源按主备排列。这个设计的价值在实际使用中才体现得出来主源在晚高峰被挤爆时播放器会自动切到备源用户几乎察觉不到。如果没有备份表现就是突然黑屏几秒钟再恢复体验差很多。实现上就是在生成 m3u 的时候把同一频道的多个来源按顺序写进去并给它们相同的频道标识。支持多源切换的播放器会自动处理。检测脚本要做的就是在排序时保证同一频道的多个来源分散在不同域名下这样才不会出现一个域名挂了三条源全挂的情况。顺带说一句题外话。最近搜 IPTV 相关内容时经常会看到一些完全不相关的工具名混在结果里比如某些安卓完整性校验工具。它们跟直播源检测没有任何关系纯属关键词污染别被带偏。认准自己要解决的问题比追着热搜词跑有用得多。最后分享一个我用了很久的小习惯把每次检测的优质源列表按日期存档只保留最近三十天的版本。这样既能看到某个频道来源的演变规律又不会让存储无限膨胀。有几次某个频道彻底失效我翻出两周前的存档发现那时候还有一条备用地址重新测一下又活了省去了重新全网搜源的功夫。