这次折腾 IPTV,一开始是想把 ImmortalWrt 上的 udpxy 换成 rtp2httpd。后来发现真正的问题不在转发工具,而在频道源本身:我先拿四川电信的节目单去跑,结果家里其实是成都/四川移动 IPTV。

现象很典型,rtp2httpd 页面能打开,播放列表也能生成,但播放器报:

1
2
Playback Error
Network error: HttpStatusCodeInvalid

这类错误容易让人先怀疑 rtp2httpd 配置、播放器、端口或者防火墙。实际根因更朴素:运营商错了,组播地址和 EPG 接口都不一样。

之前因为 IPTV 机顶盒和遥控器都太丑,我一直没和运营商要机顶盒。直到后来看到罗磊的《广东电信 IPTV 组播转单播极限测试》,才意识到家里的 IPTV 不一定只能绑在运营商机顶盒上,也可以通过组播转单播给 Apple TV、手机和电视 App 使用。

网口怎么接到 ImmortalWrt、IPTV 口怎么配置、旁路由怎么参与组播转单播,这部分我最开始参考的是《家庭 iPTV 改造》。那篇已经把家庭网络改造、光猫 iTV 口、旁路由和组播转换讲得很完整了,我这里不重复写。

这篇只记录我这次真正踩坑并解决问题的部分:

1
2
3
4
5
如何用 Wireshark 抓机顶盒流量
如何用过滤器精准找到频道列表接口
如何从 EPG JSON 里提取四川移动 IPTV 的播放地址
为什么第一次只抓到 53 个频道
如何生成给 rtp2httpd 使用的 M3U

EPG 是什么

EPG 是 Electronic Program Guide,中文一般叫电子节目指南。

在 IPTV 里,它不只是电视上看到的节目表,更像机顶盒的入口系统。机顶盒通过 EPG 页面和接口拿到频道分类、频道列表、直播地址、回看地址、节目预告、海报和一些播放参数。

可以粗略理解成:

1
2
3
EPG 负责告诉机顶盒:有哪些频道,每个频道叫什么,播放地址是什么。
组播流负责真正传视频:rtp://239.x.x.x:5140。
M3U 是我们把 EPG 里的频道地址整理成播放器能读的清单。

所以这次抓包的目标不是直接抓视频内容,而是抓出机顶盒请求 EPG 时返回的频道 JSON。

抓包接线

我用的是一个 USB 抓包设备,它有三侧:

1
2
3
光猫 IPTV 口 <-> 抓包设备 <-> 机顶盒
|
电脑 USB 网卡

这种设备的两个直通网口是背靠背直连的,侧边 USB 口给电脑监听。电脑接在哪一侧很重要:

1
2
电脑监听靠光猫一侧:更容易看到光猫/IPTV 平台返回给机顶盒的数据
电脑监听靠机顶盒一侧:更容易看到机顶盒发出的请求

我这次的重点是拿频道列表,所以更关心返回内容。接好后,macOS 里会出现一个 USB 网卡,我这里是:

1
en5 - USB 10/100/1000 LAN

打开 Wireshark,选择这个网卡开始抓包。

抓包时要做什么

只让机顶盒开机是不够的。很多 IPTV 页面是懒加载的,只有进入具体页面、打开频道列表、切换分类时,机顶盒才会请求对应数据。

我当时的操作顺序是:

1
2
3
4
5
6
7
8
9
1. 开始 Wireshark 抓包
2. 机顶盒上电
3. 进入 IPTV 首页
4. 进入直播
5. 打开频道列表
6. 切到“全部”分类
7. 上下滚动频道列表
8. 随便切几个频道播放
9. 停止抓包并保存 pcap

这里最关键的是第 6、7 步。如果只停留在首页,或者只打开默认分类,很可能只能抓到部分频道。

如何找频道列表呢?

如果只是想先找到频道列表,不建议从 dns 开始一个个包翻。dns 过滤后仍然会有很多域名,看起来很容易懵。

我自己最精准的 filter 条件是:

1
http.response && http.file_data contains "全部" && http.file_data contains "rtp"

这条命了的意思是:

1
2
3
http.response              只看服务器返回给机顶盒的 HTTP 响应
http.file_data contains "全部" 响应正文里包含“全部”这个频道分类
http.file_data contains "rtp" 响应正文里包含 rtp 播放地址

为什么它准?因为我们要找的不是任意 JSON,而是“全部频道分类 + RTP 播放地址”同时出现的响应。只满足其中一个条件都不够:

1
2
3
4
只有 JSON:可能是广告、配置、日志、鉴权结果
只有 rtp:可能只是某个局部分类
只有“全部”:可能只是页面文案
同时有“全部”和 rtp:更像完整频道列表

在我的抓包里,这条过滤器直接只剩一个包:

1
2
3
4
No. 1205
Source: 183.223.110.220
Destination: 10.76.70.200
Info: HTTP/1.1 200 OK, JSON (application/json)

这就是最关键的频道列表响应。

点中这个包后,Wireshark 里可以这样看内容:

1
2
3
4
5
1. 点击 No. 1205 这一行
2. 看下方 Packet Details
3. 展开 Hypertext Transfer Protocol
4. 找 File Data / De-chunked entity body / JSON 这类字段
5. 在里面搜索 livePlayUrl、multicastUrl、catgName

也可以右键这个包:

1
Follow -> HTTP Stream

然后在弹出的窗口里看服务端返回的 JSON,复制或保存出来。

如果没命中怎么办?

如果上述filter没有结果,通常有几种可能:

1
2
3
4
机顶盒没有切到“全部”频道分类
频道列表还没加载完成
HTTP 响应被压缩或 Wireshark 没有正确解码
不同运营商字段名不一样

这时再放宽过滤条件,一层一层找:

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 可能出现在页面、日志、推荐位里;而 livePlayUrlmulticastUrlchannels 更像频道列表接口里的结构化字段。

最终我在关键 HTTP JSON 响应里看到了类似结构:

1
2
3
4
5
6
7
8
9
10
{
"resultCode": "000",
"resultMessage": "成功",
"content": {
"category": {
"catgName": "全部"
},
"channels": []
}
}

展开 channels,每个频道里都有频道名、频道号和播放地址。简化后大概是:

1
2
3
4
5
6
7
{
"num": "1",
"name": "CCTV-1",
"multicastUrl": "rtp://239.11.0.123:5140?channelFECPort=5142&ChannelFCCPort=8027&ChannelFCCIP=183.223.164.123",
"livePlayUrl": "rtp://239.11.0.123:5140?ChannelFCCPort=8027&ChannelFCCIP=183.223.164.123&channelFECPort=5142",
"timeShiftAlias": "rtsp://223.87.18.123:1556/11111111/ch19052405404444840887"
}

这就是我们要找的东西。

DNS 是辅助线索

DNS 不是用来直接找频道列表的,它更像通讯录:告诉我们某个 IP 对应哪个域名。

比如先通过上面的 HTTP 过滤器找到频道包来自:

1
183.223.113.123

再回头查 DNS,就可以用更精确的过滤器:

1
dns.a == 183.223.113.123

这样就能看到这个 IP 曾经对应过哪些域名。我的抓包里能看到:

1
2
bims.scmcc.sctv.com -> 183.223.113.123
ottcn.help -> 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
2
3
jtdsepg.sctv.com
bims.scmcc.sctv.com
ottcn.help

jtdsepg.sctv.com 这个名字里带 epg,很容易第一眼就盯上它。它确实像 EPG 入口,返回过 text/html、CSS、JavaScript 这些前端资源。

但真正的频道列表不一定就在这个域名本身。判断一个服务器是不是“频道接口服务器”,不能只看域名像不像,而要看响应内容里是否真的有 catgName=全部channelslivePlayUrlmulticastUrlrtp://239...

为什么第一次只抓到 53 个频道

第一次我也搜到了包含 rtp://239 的对象,还以为已经找到频道列表了。但导出来一看,只有 53 个频道。

后来回头看字段,问题出在分类上:

1
2
catgName: 最新
channels: 53

也就是说,那不是完整频道表,只是“最新”分类。

真正应该找的是:

1
catgName: 全部

catgName=全部 的 JSON 里,我这次抓到了 109 个直播频道。频道号最大能看到 266,但中间有很多空档,所以不能把最大频道号当成频道总数。

这个坑很有代表性:

1
2
3
搜到 rtp://239 不代表找到了完整节目单。
看到频道号 266 不代表有 266 个频道。
一定要确认当前 JSON 对应的是哪个分类。

导出 HTTP 对象

Wireshark 里可以把 HTTP 对象直接导出来:

1
File -> Export Objects -> HTTP

保存后,在导出目录里搜索:

1
rg 'livePlayUrl|multicastUrl|rtp://239|catgName|channels' .

如果有多个对象都包含频道字段,我会按这个顺序判断:

1
2
3
4
5
1. 是否有 resultCode/resultMessage 这类接口返回结构
2. 是否有 content.category.catgName
3. catgName 是否为“全部”
4. channels 数组里是否有 name/num/livePlayUrl/multicastUrl
5. 播放地址是否是 rtp://239.x.x.x:5140 这类组播地址

这比肉眼在 Wireshark 里翻包稳定很多。

M3U 怎么生成

拿到频道 JSON 后,不一定要自己写脚本解析。最省事的方法是:把 Wireshark 里看到的 HTTP 返回值复制出来,直接发给 AI,让 AI 按规则转成 M3U。

操作上可以这样做:

1
2
3
4
1. 在 Wireshark 里点中频道 JSON 那个包
2. 右键 Follow -> HTTP Stream
3. 复制服务端返回的 JSON
4. 发给 AI,让它转换成 M3U

我会把提示词写得非常明确,避免 AI 取错字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
下面是一段 IPTV EPG 返回的 JSON,请帮我转换成 M3U。

要求:
1. 只输出 M3U 内容,不要解释。
2. 第一行必须是 #EXTM3U。
3. 从 channels 数组里提取频道。
4. 频道名取频道对象外层的 name。
5. 不要把“标清”“高清”“超清”这种清晰度 name 当频道名。
6. tvg-id 优先使用 num,没有 num 就用频道顺序。
7. tvg-name 和逗号后的显示名都使用频道名。
8. 播放地址优先使用 multicastUrl;没有 multicastUrl 再使用 livePlayUrl。
9. 将 ChannelFCCIP、ChannelFCCPort、channelFECPort 转成 rtp2httpd 支持的 fcc/fec 参数。
10. 跳过没有 rtp://239 地址的频道。

输出格式示例:
#EXTM3U
#EXTINF:-1 tvg-id="1" tvg-name="CCTV-1",CCTV-1
rtp://239.11.0.65:5140/?fcc=183.223.164.123:8027&fec=5142

这里最容易错的是频道名。有些频道对象下面还会嵌套清晰度列表,里面也有 name 字段:

1
2
3
4
5
{
"uuid": "ysten-cctv-1",
"name": "标清",
"url": "rtp://239.11.0.65:5140?ChannelFCCPort=8027&ChannelFCCIP=183.223.164.123&channelFECPort=5142"
}

这个 "标清" 是清晰度,不是频道名。频道名应该取外层频道对象里的 "CCTV-1"

AI 转出来以后,再肉眼检查几项:

1
2
3
4
5
6
第一行是不是 #EXTM3U
每个频道是不是两行:#EXTINF + rtp 地址
频道名有没有变成“标清”“高清”
rtp://239.x.x.x:5140 地址是否还在
FCC/FEC 参数是否转成 fcc=IP:PORT&fec=PORT
频道数量是否接近 JSON 里的 channels 数量

基础 M3U 可以长这样:

1
2
3
#EXTM3U
#EXTINF:-1 tvg-id="1" tvg-name="CCTV-1",CCTV-1
rtp://239.11.0.65:5140

如果想保留快速起播和纠错相关参数,不建议继续使用 EPG 里的原始字段名,而是转成 rtp2httpd 支持的写法:

1
2
3
#EXTM3U
#EXTINF:-1 tvg-id="1" tvg-name="CCTV-1",CCTV-1
rtp://239.11.0.65:5140/?fcc=183.223.164.123:8027&fec=5142

这里的对应关系是:

1
2
3
ChannelFCCIP   -> fcc 的 IP
ChannelFCCPort -> fcc 的端口
channelFECPort -> fec 的端口

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
2
3
#EXTM3U
#EXTINF:-1 tvg-id="1" tvg-name="CCTV-1",CCTV-1
rtp://239.11.0.65:5140/?fcc=183.223.164.65:8027&fec=5142

我最后给 rtp2httpd 使用的是第二种:外部 M3U 里写 rtp://.../?fcc=...&fec=...,再由 rtp2httpd 生成给播放器使用的 HTTP 播放列表。

验证 M3U 能不能播放

频道 JSON 只能说明 EPG 返回了地址,真正能不能用,最简单的验证方式不是继续抓包,而是把生成好的 M3U 交给 rtp2httpd,直接点 Web 播放器。

我的验证路径是:

1
2
3
4
5
1. 把 AI 生成的 M3U 保存到 ImmortalWrt
2. 在 rtp2httpd 里把 external_m3u 指向这份 M3U
3. 重启 rtp2httpd
4. 打开 http://192.168.50.2:4022/player
5. 在 Web 播放器里点一个频道

如果能起播,说明这几件事都通了:

1
2
3
4
5
M3U 格式没有问题
rtp2httpd 能读到这份 M3U
路由器能从 IPTV 网口加入组播
rtp2httpd 能把 RTP 转成本地 HTTP
浏览器能从 LAN 访问播放地址

如果 Web 播放器打不开,再回头查:

1
2
3
4
M3U 里是不是 rtp://239.x.x.x:5140
rtp2httpd 配置的 external_m3u 路径是否正确
rtp2httpd 服务有没有重启
上游组播接口是不是 IPTV 那张网卡

这一步比单独看包更直观。对小白来说,只要 rtp2httpd 自带播放器能播放,后面再把 /playlist.m3u 给电视 App 使用就顺理成章了。

rtp2httpd 的 playlist 为什么看不到 FCC

我当时还有一个疑问:原始 M3U 里明明有:

1
2
fcc=183.223.164.65:8027
fec=5142

但访问 rtp2httpd 的播放列表:

1
curl http://192.168.50.2:4022/playlist.m3u

看到的是本地 HTTP 地址:

1
2
3
#EXTM3U
#EXTINF:-1 tvg-id="1" tvg-name="CCTV-1",CCTV-1
http://192.168.50.2:4022/CCTV-1

这其实正常。/playlist.m3u 是给播放器看的 HTTP 入口,不会把原始 rtp://...?... 参数原样暴露出来。要确认 FCC 参数有没有进源文件,应该看 rtp2httpd 读取的原始 M3U:

1
2
grep 'fcc=' /root/iptv/sichuan-mobile-capture-fcc.m3u
uci get rtp2httpd.@rtp2httpd[0].external_m3u

如果要进一步确认 FCC 连接,可以在 IPTV 网口抓对应地址:

1
tcpdump -ni eth1 host 183.223.164.123

最后的收获

这次真正有价值的不是某条命令,而是排查顺序。

我的结论是:

1
2
3
4
5
6
7
1. 先确认运营商,不要拿四川电信源去跑四川移动 IPTV
2. 抓包时必须触发机顶盒加载“全部”频道
3. 优先用 http.file_data contains "全部" && http.file_data contains "rtp" 找频道包
4. DNS 只作为辅助,用来解释目标 IP 对应哪个域名
5. 不要只搜第一个 rtp://239,要确认它属于 catgName=全部
6. 把 HTTP 返回的 JSON 发给 AI 转 M3U,但要检查频道名和 FCC/FEC 参数
7. 把 M3U 导入 rtp2httpd,用 Web 播放器点频道验证

最终拿到的是 109 个直播频道。它们不是网上抄来的公共节目单,而是从自己家四川移动 IPTV 机顶盒实际请求里抓出来的频道列表。

对我来说,这比直接复制一份 M3U 更踏实:以后源失效了,也知道应该回到哪里重新抓、怎么判断哪个包才是真的频道列表。

参考阅读