这次折腾 IPTV,一开始是想把 ImmortalWrt 上的 udpxy 换成 rtp2httpd。后来发现真正的问题不在转发工具,而在频道源本身:我先拿四川电信的节目单去跑,结果家里其实是成都/四川移动 IPTV。
现象很典型,rtp2httpd 页面能打开,播放列表也能生成,但播放器报:
1 | Playback Error |
这类错误容易让人先怀疑 rtp2httpd 配置、播放器、端口或者防火墙。实际根因更朴素:运营商错了,组播地址和 EPG 接口都不一样。
之前因为 IPTV 机顶盒和遥控器都太丑,我一直没和运营商要机顶盒。直到后来看到罗磊的《广东电信 IPTV 组播转单播极限测试》,才意识到家里的 IPTV 不一定只能绑在运营商机顶盒上,也可以通过组播转单播给 Apple TV、手机和电视 App 使用。
网口怎么接到 ImmortalWrt、IPTV 口怎么配置、旁路由怎么参与组播转单播,这部分我最开始参考的是《家庭 iPTV 改造》。那篇已经把家庭网络改造、光猫 iTV 口、旁路由和组播转换讲得很完整了,我这里不重复写。
这篇只记录我这次真正踩坑并解决问题的部分:
1 | 如何用 Wireshark 抓机顶盒流量 |
EPG 是什么
EPG 是 Electronic Program Guide,中文一般叫电子节目指南。
在 IPTV 里,它不只是电视上看到的节目表,更像机顶盒的入口系统。机顶盒通过 EPG 页面和接口拿到频道分类、频道列表、直播地址、回看地址、节目预告、海报和一些播放参数。
可以粗略理解成:
1 | EPG 负责告诉机顶盒:有哪些频道,每个频道叫什么,播放地址是什么。 |
所以这次抓包的目标不是直接抓视频内容,而是抓出机顶盒请求 EPG 时返回的频道 JSON。
抓包接线
我用的是一个 USB 抓包设备,它有三侧:
1 | 光猫 IPTV 口 <-> 抓包设备 <-> 机顶盒 |
这种设备的两个直通网口是背靠背直连的,侧边 USB 口给电脑监听。电脑接在哪一侧很重要:
1 | 电脑监听靠光猫一侧:更容易看到光猫/IPTV 平台返回给机顶盒的数据 |
我这次的重点是拿频道列表,所以更关心返回内容。接好后,macOS 里会出现一个 USB 网卡,我这里是:
1 | en5 - USB 10/100/1000 LAN |
打开 Wireshark,选择这个网卡开始抓包。
抓包时要做什么
只让机顶盒开机是不够的。很多 IPTV 页面是懒加载的,只有进入具体页面、打开频道列表、切换分类时,机顶盒才会请求对应数据。
我当时的操作顺序是:
1 | 1. 开始 Wireshark 抓包 |
这里最关键的是第 6、7 步。如果只停留在首页,或者只打开默认分类,很可能只能抓到部分频道。
如何找频道列表呢?
如果只是想先找到频道列表,不建议从 dns 开始一个个包翻。dns 过滤后仍然会有很多域名,看起来很容易懵。
我自己最精准的 filter 条件是:
1 | http.response && http.file_data contains "全部" && http.file_data contains "rtp" |
这条命了的意思是:
1 | http.response 只看服务器返回给机顶盒的 HTTP 响应 |
为什么它准?因为我们要找的不是任意 JSON,而是“全部频道分类 + RTP 播放地址”同时出现的响应。只满足其中一个条件都不够:
1 | 只有 JSON:可能是广告、配置、日志、鉴权结果 |
在我的抓包里,这条过滤器直接只剩一个包:
1 | No. 1205 |
这就是最关键的频道列表响应。
点中这个包后,Wireshark 里可以这样看内容:
1 | 1. 点击 No. 1205 这一行 |
也可以右键这个包:
1 | Follow -> HTTP Stream |
然后在弹出的窗口里看服务端返回的 JSON,复制或保存出来。
如果没命中怎么办?
如果上述filter没有结果,通常有几种可能:
1 | 机顶盒没有切到“全部”频道分类 |
这时再放宽过滤条件,一层一层找:
1 | http.response && http.content_type contains "json" |
这条会显示所有 JSON 响应,数量会比较多。不要每个包都点开看,继续加关键词缩小范围:
1 | http.response && http.file_data contains "channels" |
1 | http.response && http.file_data contains "livePlayUrl" |
1 | http.response && http.file_data contains "multicastUrl" |
1 | http.response && http.file_data contains "catgName" |
如果这些字段都能搜到,再组合成更精确的过滤器:
1 | http.response && http.file_data contains "catgName" && http.file_data contains "rtp" |
这些关键词比单纯搜索 CCTV 更有用。CCTV 可能出现在页面、日志、推荐位里;而 livePlayUrl、multicastUrl、channels 更像频道列表接口里的结构化字段。
最终我在关键 HTTP JSON 响应里看到了类似结构:
1 | { |
展开 channels,每个频道里都有频道名、频道号和播放地址。简化后大概是:
1 | { |
这就是我们要找的东西。
DNS 是辅助线索
DNS 不是用来直接找频道列表的,它更像通讯录:告诉我们某个 IP 对应哪个域名。
比如先通过上面的 HTTP 过滤器找到频道包来自:
1 | 183.223.113.123 |
再回头查 DNS,就可以用更精确的过滤器:
1 | dns.a == 183.223.113.123 |
这样就能看到这个 IP 曾经对应过哪些域名。我的抓包里能看到:
1 | bims.scmcc.sctv.com -> 183.223.113.123 |
如果还不知道具体 IP,只想先看可疑域名,可以用这些关键词缩小 DNS:
1 | dns && dns.qry.name contains "epg" |
1 | dns && dns.qry.name contains "sctv" |
1 | dns && dns.qry.name contains "scmcc" |
1 | dns && dns.qry.name contains "ottcn" |
这次出现了几个关键域名:
1 | jtdsepg.sctv.com |
jtdsepg.sctv.com 这个名字里带 epg,很容易第一眼就盯上它。它确实像 EPG 入口,返回过 text/html、CSS、JavaScript 这些前端资源。
但真正的频道列表不一定就在这个域名本身。判断一个服务器是不是“频道接口服务器”,不能只看域名像不像,而要看响应内容里是否真的有 catgName=全部、channels、livePlayUrl、multicastUrl 和 rtp://239...。
为什么第一次只抓到 53 个频道
第一次我也搜到了包含 rtp://239 的对象,还以为已经找到频道列表了。但导出来一看,只有 53 个频道。
后来回头看字段,问题出在分类上:
1 | catgName: 最新 |
也就是说,那不是完整频道表,只是“最新”分类。
真正应该找的是:
1 | catgName: 全部 |
在 catgName=全部 的 JSON 里,我这次抓到了 109 个直播频道。频道号最大能看到 266,但中间有很多空档,所以不能把最大频道号当成频道总数。
这个坑很有代表性:
1 | 搜到 rtp://239 不代表找到了完整节目单。 |
导出 HTTP 对象
Wireshark 里可以把 HTTP 对象直接导出来:
1 | File -> Export Objects -> HTTP |
保存后,在导出目录里搜索:
1 | rg 'livePlayUrl|multicastUrl|rtp://239|catgName|channels' . |
如果有多个对象都包含频道字段,我会按这个顺序判断:
1 | 1. 是否有 resultCode/resultMessage 这类接口返回结构 |
这比肉眼在 Wireshark 里翻包稳定很多。
M3U 怎么生成
拿到频道 JSON 后,不一定要自己写脚本解析。最省事的方法是:把 Wireshark 里看到的 HTTP 返回值复制出来,直接发给 AI,让 AI 按规则转成 M3U。
操作上可以这样做:
1 | 1. 在 Wireshark 里点中频道 JSON 那个包 |
我会把提示词写得非常明确,避免 AI 取错字段:
1 | 下面是一段 IPTV EPG 返回的 JSON,请帮我转换成 M3U。 |
这里最容易错的是频道名。有些频道对象下面还会嵌套清晰度列表,里面也有 name 字段:
1 | { |
这个 "标清" 是清晰度,不是频道名。频道名应该取外层频道对象里的 "CCTV-1"。
AI 转出来以后,再肉眼检查几项:
1 | 第一行是不是 #EXTM3U |
基础 M3U 可以长这样:
1 | #EXTM3U |
如果想保留快速起播和纠错相关参数,不建议继续使用 EPG 里的原始字段名,而是转成 rtp2httpd 支持的写法:
1 | #EXTM3U |
这里的对应关系是:
1 | ChannelFCCIP -> fcc 的 IP |
rtp2httpd 官方支持的 FCC 写法
rtp2httpd 官方文档里,FCC 可以写在直接访问 URL 里,也可以写在外部 M3U 里。
如果直接用 HTTP URL 点播组播流,格式是:
1 | http://rtp2httpd地址:端口/rtp/组播地址:端口?fcc=FCC服务器IP:端口&fec=FEC端口 |
例如:
1 | http://192.168.50.2:4022/rtp/239.11.0.65:5140?fcc=183.223.164.65:8027&fec=5142 |
如果使用 rtp2httpd 的 M3U 转换功能,外部 M3U 里可以直接写 RTP 源:
1 | #EXTM3U |
我最后给 rtp2httpd 使用的是第二种:外部 M3U 里写 rtp://.../?fcc=...&fec=...,再由 rtp2httpd 生成给播放器使用的 HTTP 播放列表。
验证 M3U 能不能播放
频道 JSON 只能说明 EPG 返回了地址,真正能不能用,最简单的验证方式不是继续抓包,而是把生成好的 M3U 交给 rtp2httpd,直接点 Web 播放器。
我的验证路径是:
1 | 1. 把 AI 生成的 M3U 保存到 ImmortalWrt |
如果能起播,说明这几件事都通了:
1 | M3U 格式没有问题 |
如果 Web 播放器打不开,再回头查:
1 | M3U 里是不是 rtp://239.x.x.x:5140 |
这一步比单独看包更直观。对小白来说,只要 rtp2httpd 自带播放器能播放,后面再把 /playlist.m3u 给电视 App 使用就顺理成章了。
rtp2httpd 的 playlist 为什么看不到 FCC
我当时还有一个疑问:原始 M3U 里明明有:
1 | fcc=183.223.164.65:8027 |
但访问 rtp2httpd 的播放列表:
1 | curl http://192.168.50.2:4022/playlist.m3u |
看到的是本地 HTTP 地址:
1 | #EXTM3U |
这其实正常。/playlist.m3u 是给播放器看的 HTTP 入口,不会把原始 rtp://...?... 参数原样暴露出来。要确认 FCC 参数有没有进源文件,应该看 rtp2httpd 读取的原始 M3U:
1 | grep 'fcc=' /root/iptv/sichuan-mobile-capture-fcc.m3u |
如果要进一步确认 FCC 连接,可以在 IPTV 网口抓对应地址:
1 | tcpdump -ni eth1 host 183.223.164.123 |
最后的收获
这次真正有价值的不是某条命令,而是排查顺序。
我的结论是:
1 | 1. 先确认运营商,不要拿四川电信源去跑四川移动 IPTV |
最终拿到的是 109 个直播频道。它们不是网上抄来的公共节目单,而是从自己家四川移动 IPTV 机顶盒实际请求里抓出来的频道列表。
对我来说,这比直接复制一份 M3U 更踏实:以后源失效了,也知道应该回到哪里重新抓、怎么判断哪个包才是真的频道列表。