上篇写到如何通过策略路由,使得可以局域网内播放单播rtsp视频。由于局域网内客户端对rstp视频支持不够友好,使得无法直接使用rtsp地址。这时候我们就需要通过rtp2httpd这个软件,将rtsp代理转发给下游客户端。
四、RTP2HTTPD 代理转发
rtp2httpd官方github开源软件地址:https://github.com/stackia/rtp2httpd

路由器上安装也是比较简单的,这点rtp2httpd 比其他开源软件做的好,Openwrt路由器上一键安装脚本:
wget -O - https://raw.githubusercontent.com/stackia/rtp2httpd/main/scripts/install-openwrt.sh | sh
安装完成后,在 LuCI 管理界面的 “服务” 菜单中找到 “rtp2httpd” 进行配置。每次更新版本后如果 LuCI 出现工作异常,需要 Ctrl+F5 刷新 或 清空浏览器缓存 或 使用无痕模式访问 解决。如果通过 OpenWrt 源码构建直接把 rtp2httpd 编译到固件里,可以把该仓库地址加入到 feeds.conf.default 文件,加入
src-git rtp2httpd https://github.com/stackia/rtp2httpd.git
运行 ./scripts/feeds update rtp2httpd 和 ./scripts/feeds install rtp2httpd 更新 feed。
运行 make menuconfig,找到 LuCI -> Applications -> luci-app-rtp2httpd,将其勾选 ,保存退出。
运行 make -j8 ,即可编译路由器固件。rtp2httpd 访问iptv服务器用的useragent是:”(最新版已经可以设置useragent了,因此无需修改源码)。rtp2httpd使用比较简单,相当不错。没有社么好说的。有问题多看几次该项目说明和Issues。rtp2httpd/版本号“,我们可以修改rtp2httpd/src/rtsp.c 文件,将约29行 #define USER_AGENT "rtp2httpd/" VERSION 修改为 #define USER_AGENT "机顶盒种rtsp服务的useragent",wireshark抓包数据里有。这样可以使得iptv服务器认为是盒子在和它通信,而不是rtp2httpd程序。

rtpshttpd网页播放、回看也没问题,
五、M3U文件编制及TIVIMATE的使用
tivimate要能看回放及时移,需要配置好M3U文件。M3U文件基本格式如下:
无需鉴权M3U文件示例:
#EXTINF:-1 tvg-name="CCTV1" catchup="default" catchup-source="rtsp://220.167.81.247/PLTV/88888896/224/3221228807/10000100000000060000000003732597_0.smil?playseek={utc:YmdHMS}-{utcend:YmdHMS}",CCTV1综合
http://192.168.3.2:5140/223.94.0.31:5140?fcc=220.167.81.247:8027含有鉴权参数的M3U文件示例:
#EXTINF:-1 tvg-name="CCTV1" catchup="default" catchup-source="rtsp://123.147.112.152:1554/iptv/import/Tvod/iptv/001/040000010400000200000001/01000000004000000000000000000231.rsc/24067_Uni.sdp?lvst=3&nodeid=iptvnode109&TSTVTimeLife=7200&userid=&userip=220.200.73.191&time=20251028171008.120+00&life=86400000&transferproviderid=040000010400000200000001&ctype=01&sid=208291671&purl=MTIzLjE0Ny4xMTIuMTUwOjU1NC8wNDAwMDAwMS8wMTAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDIzMT8&domain=0&phycid=01000000004000000000000000000231&playtype=1&urtm=0&agnt=MTIzLjE0Ny4xMTIuMTUwOjU1NA&sadr=MTIzLjE0Ny4xMTIuMTUy&iptvsessionid=Agent1661120251028171008125090&cryptmode=0502&crypt=3221E7F9A6E4909B75F204E2605D07C0&playseek={utc:YmdHMS}-{utcend:YmdHMS}
http://192.168.3.2:5140/223.94.0.31:5140?fcc=220.167.81.247:8027第一行是EXTINF信息行(频道信息行),比较重要的是catchup="default" catchup-source="单播地址",当你回放或时移时会用该单播地址播放。
第二行是 媒体播放地址,一般写入组播地址,可以快速播放,这是直播LIVE地址。如果需要FCC 快速换台,在组播地址后方加入 ?fcc=fcc服务器:端口号 。
注意点:
1、playseek={utc:YmdHMS}-{utcend:YmdHMS} 是时移参数,播放器会自动填充数值,拖动进度条,就会变为playseek=20260119090655-20260119090928。要看回放需搞清本地IPTV服务器到底用的是UTC时间还是本地时间(UTC+8)。playseek={utc:YmdHMS}-{utcend:YmdHMS} 这是UTC时间。如果不能播放,可以用playseek={(b)YmdHMS}-{(e)YmdHMS} 尝试,这是本地时间(UTC+8)。对于IPTV服务器用的Unix 时间戳,则应该为 playseek=${(b)timestamp}-${(e)timestamp} 。还有用 playseek={utc}-{utcend} UTC,ISO8601 格式的。有的iptv服务器还不认 playseek ,需要改为 tvdr ,也就是 ?tvdr={utc:YmdHMS}-{utcend:YmdHMS} 。具体如何设置,多多尝试或搜索本地用户的文章。具体可查看 https://github.com/stackia/rtp2httpd/blob/main/docs/m3u-integration.md
2、catchup=”default” 播放器 直接使用 catchup-source 后的链接地址。
这个catchup还可以写为catchup=”append” catchup-source=”?playseek={utc:YmdHMS}-{utcend:YmdHMS}”,(如果下方 媒体播放地址 中含有?此处就不能写 ?应该改为 & ,链接中最多一个?,表示后面的是各种参数)
这就表明播放器播放时移单播视频的时候,用的是组合媒体播放地址+catchup-source组合的链接,也就是最后链接为:
http://192.168.3.2:5140/223.94.0.31:5140?playseek={utc:YmdHMS}-{utcend:YmdHMS}。
含有fcc快速播放的地址也是:
http://192.168.3.2:5140/223.94.0.31:5140?playseek={utc:YmdHMS}-{utcend:YmdHMS} 。rtp2httpd会自动转换url地址,写的不合理,就会无法播放。
如果你写成:
#EXTINF:-1 tvg-name=”CCTV1″ catchup=”append” catchup-source=”rtsp://220.167.81.247/PLTV/88888896/224/3221228807/1003732597_0.smil?authinfo=”1234″&playseek={utc:YmdHMS}-{utcend:YmdHMS}”,CCTV1综合
http://192.168.3.2:5140/223.94.0.31:5140?fcc=220.167.81.247:8027那么经过rtp2httpd转换成的单播url就变为 :
/223.94.0.31:5140/rtsp/220.167.81.247/PLTV/88888896/224/3221228807/1003732597_0.smil?authinfo=”1234″&playseek={utc:YmdHMS}-{utcend:YmdHMS}
那么显然是无法播放的。所以尽量不要用 catchup=”append” 。
Tivimate使用的难点
我则是正版tivimate,tivimate还是不支持直接播放rtsp,写M3U文件这里 catchup-source 可以采用rtp2httd的http流格式: catchup-source="http://192.168.1.1:5140/rtsp/1.2.3.4/1/000011234?AuthInfo=azBUz&playseek={utc:YmdHMS}-{utcend:YmdHMS}" 。不能直接写成 rtsp://220.167.81.247:554/....../?....playseek={utc:YmdHMS}-{utcend:YmdHMS} ,这种播放不了。
一顿操作下来,以为稳稳地可以时移观看了,实际tivimate种无法时移,但是可以回放几小时前的内容。这就奇怪了,明明rtp2httpd的网页播放器可以实现时移,但是到我这里就不行,网上也没有我这种情况的帖子。
对rtp2httpd的网页播放器 进行F12 浏览器分析,粗略发现它发送的时间格式是 playseek=UTC时移的时间-UTC当前时间 。看rtp2httpd 的日志,tivimata 播放连接中是 playseek=时移的时间-epg节目结束时间 。
看来是时间有误,将参数改为 playseek={utc:YmdHMS}-{now:YmdHMS} 也就是 节目开始时间-当前时间 ,这下怪了,连rtp2httpd的网页播放器 也不能播放了,tivimate更不用说了。查看了很多tivimate使用的格式,很多都说用 playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss} (节目开始时间-epg节目本地结束时间),测试了下,tivimate 连时间解析都解析不出来,直接发送链接 http://192.168.1.1:5140/rtsp/....../?...&playseek={(b)yyyyMMddHHmmss}-{(e)yyyyMMddHHmmss} 到rtp2httpd服务端了。修改 tivimate User Agent 设置,加上 TZ/UTC+8 ,rtp2httpd将识别为国内时区,回放时移倒是可以了,但是是8小时前的节目。将这下凌乱了,难道是tivimate 不支持还是时移地址不对抑或是M3U文件写法有问题?到reddit tivimate论坛搜索了以下,没人碰到此等问题, 为解决此问题,各种方法测试了一天还是无解。
第二天,总结并详细了解了关于时间占位符的各种设定,痛定思痛,决定分析 rtp2httpd的网页播放器 的js脚本,看看为什么网页端可以时移,它采取了什么措施。原来 rtp2httpd 对时移采用了如下处理,它取 epg节目结束时间和当前时间的最小值,并减去10秒作为最终endtime。本地IPTV服务器不接受未来时间和当前时间,endtime 最多是当前时间-10s。如此就顺利找到了解决此问题的方法。时移回看改为:playseek={utc:YmdHMS}-{lutc:YmdHMS}&r2h-seek-offset=-15,多给了5秒,也就是 UTC时移的时间—–UTC当前时间-15s。可惜tivimate不支持这种取最小值的方法,但是不影响观看,现在7天回放都可以了,问题圆满解决。在实际观看中,发现tivimate时移几分钟前的节目会略有卡顿,可能是tivimate在重新生成播放链接请求IPTV服务器或这个-15s在作怪。
六、自动获取IPTV单播地址
我这里IPTV单播带鉴权的地址鉴权信息估计有有效期(还没尝试到过7天后鉴权信息是否有效),不能获取到新的鉴权信息过段时间rtsp地址将无法播放。IPTV口IP信息发生变化也无法用原来的rtsp链接,需要重新生成新的带鉴权的rtsp单播链接。这里参考了这篇帖子 https://www.right.com.cn/forum/thread-8314231-1-1.html 。根据wireshark抓包数据里的地址及POST、GET所用参数用shell 模拟盒子获得鉴权信息。过程总体是顺利的,有了AI,DES3 ECB 加密生成密文UserToken 成为了可能。下面就是半天时间写的脚本,各种值的说明脚本里都有,如果你需要可以根据你wireshark抓包的数据进行分析,wireshark 输入框里输入 ip.addr == 盒子ip &&(http||rtsp) 进行查找。

这个版本只是自动生成了简单的m3u播放文件和简单的文本文件。并没有考虑台标等这些问题,可以用一个M3U模板,然后脚本修改替换单播信息,实际我也是这么处理的,由于M3U模板各不一致,就不放出来了。
#!/bin/sh
############################
# 必填(抓包 / 盒子获取)
############################
KEY="" # iptv密码,8位
USERID="" # IPTV账号,不含@vod,后面拼接了@vod
STBID="" # 机顶盒序列号,如:2A000000000000000000000000000060,机顶盒铭牌出或抓包处有
MAC="" # 机顶盒MAC地址
GOT_FILE="/usr/share/myfile/iptv" # 自定义保存的各种网页文件、TOKEN等
ITV_HOST="" # 认证服务器地址,GET /auth?UserID= 处的主机名,如:http://1.2.3.4:8298",非ip的域名注意用运行商dns解析
IPTVINTERFACE="" # IPTV网卡接口,如eth3、eth1.2等
USERAGENT="" # 机顶盒的用户代理,如:ZTC HUAWEI Resolution(PAL,720p,1080i) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7
ROUTEIP="" # rtp2httpd地址,格式 http://192.168.1.1:5140
echolog() {
local d="$(date "+%Y-%m-%d %H:%M:%S")"
echo "$d: [IPTV]$*" >> "/usr/share/myfile/iptv.log"
}
# 获取iptv接口ip,本例在eth4口上
get_iptvip() {
IP=$(ip -4 addr show dev ${IPTVINTERFACE} scope global | awk '/inet / {print $2}' | cut -d/ -f1)
}
############################
# DES3 ECB 加密(HEX)
############################
des3_encrypt() {
# params: plaintext (string), key (optional)
plain="$1"
k="$2"
[ -z "$k" ] && k="$key"
if [ -z "$k" ]; then
printf '%s' "ERR:no key provided" >&2
return 1
fi
# build 24-byte ASCII key: key + 16 ASCII '0'
key24="$k"
i=0
while [ "$i" -lt 16 ]; do
key24="${key24}0"
i=$((i+1))
done
# compute padlen (PKCS#7 style for blocksize=8)
len=$(printf '%s' "$plain" | wc -c)
mod=$((len % 8))
padlen=$((8 - mod))
if [ "$mod" -eq 0 ]; then
padlen=8
fi
tf_plain="$(mktemp -t getiptv_plain.XXXXXX 2>/dev/null || echo /tmp/getiptv_plain.$$)"
printf '%s' "$plain" > "$tf_plain"
awk -v n="$padlen" -v v="$padlen" 'BEGIN{ for(i=0;i<n;i++) printf "%c", v }' >> "$tf_plain"
# convert key24 (ASCII) to hex
keyhex="$(printf '%s' "$key24" | hexdump -v -e '/1 "%02x"')"
# encrypt using openssl (3DES ECB)
tf_out="$(mktemp -t getiptv_out.XXXXXX 2>/dev/null || echo /tmp/getiptv_out.$$)"
if ! openssl enc -des-ede3 -nosalt -nopad -K "$keyhex" -in "$tf_plain" -out "$tf_out" 2>/dev/null; then
rm -f "$tf_plain" "$tf_out"
printf '%s' "ERR:openssl encrypt failed" >&2
return 2
fi
hexdump -v -e '/1 "%02x"' "$tf_out"
rm -f "$tf_plain" "$tf_out"
return 0
}
############################
# 1. 获取 CTCGetAuthInfo
############################
get_ctc_token() {
URL="$ITV_HOST/auth?UserID=${USERID}&Action=Login"
PAGE=$(curl -s -L -A "$USERAGENT" "$URL" | tee $GOT_FILE/iptvlogin)
CTC_TOKEN=$(echo "$PAGE"| sed -n "s/.*CTCGetAuthInfo('\([^']*\)').*/\1/p")
if [ -z "$CTC_TOKEN" ]; then
echolog "[-] 获取 CTCGetAuthInfo 失败"
exit 1
else
echolog "当前 CTCGetAuthInfo 是 $CTC_TOKEN"
fi
}
############################
# 2. 生成 Authenticator
############################
make_authenticator() {
RAND=$(tr -dc 0-9 </dev/urandom | head -c8)
SESSION_REF="${RAND}\$${CTC_TOKEN}\$${USERID}\$${STBID}\$${IP}\$${MAC}\$\$CTC"
#echo "$SESSION_REF"
AUTHENTICATOR=$(des3_encrypt "$SESSION_REF" "$KEY")
if [ -z "$AUTHENTICATOR" ]; then
echolog "[-] DES3 加密失败"
exit 1
else
echolog "当前加密生成的 AUTHENTICATOR 是 $AUTHENTICATOR "
fi
}
############################
# 3. 获取 usertoken 作为cookie
############################
get_usertoken() {
RESP=$(curl -s \
-c $GOT_FILE/usertoken.cookie \
-A "$USERAGENT" \
-d "UserID=${USERID}" \
-d "Authenticator=${AUTHENTICATOR}" \
-d "AccessMethod=dhcp" \
-d "AccessUserName=${USERID}@vod" \
"$ITV_HOST/uploadAuthInfo" | tee $GOT_FILE/iptvuploadAuthInfo)
USERTOKEN=$(echo "$RESP" | sed -n "s/.*UserToken','\([^']*\)'.*/\1/p")
if [ -z "$USERTOKEN" ]; then
echo "[-] USERTOKEN 获取失败"
exit 1
else
echolog "当前 usertoken 是 $USERTOKEN"
fi
}
############################
# 4、得到 JSESSIONID
############################
get_session() {
RESP=$(curl -s \
-A "$USERAGENT" \
-b "UserToken=$USERTOKEN" \
"$ITV_HOST/getServiceList" | tee $GOT_FILE/getServiceList)
UserGroupNMBURL=$(echo "$RESP" | sed -n "s/.*location='\(http:\/\/[^\']*\)'.*/\1/p")
if [ -z "$UserGroupNMBURL" ]; then
echolog "[-] UserGroupNMB 链接获取失败"
exit 1
else
echolog "当前 UserGroupNMB 链接是 $UserGroupNMBURL"
fi
RESP=$(curl -s \
-A "$USERAGENT" \
"$UserGroupNMBURL" | tee $GOT_FILE/UserGroupNMB )
loadbancedurl=$(echo "$RESP" | sed -n "s/.*location[[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p")
if [ -z "$loadbancedurl" ]; then
echolog "[-] loadbancedurl 获取失败"
exit 1
else
echolog "当前 loadbancedurl 是 $loadbancedurl"
fi
# 获取新负载均衡服务器地址
AUTHHOST=$(echo "$loadbancedurl" | cut -d/ -f1-3)
# 获得cookie,从响应头获取
JSESSIONID=$(curl -s -A "$USERAGENT" -D - "$loadbancedurl" -o $GOT_FILE/loadbancedurl \
| grep -i '^Set-Cookie:' \
| awk '{print $2}' \
| cut -d';' -f1)
echo "$JSESSIONID" > $GOT_FILE/JSESSIONID
if [ -z "$JSESSIONID" ]; then
echolog "[-] JSESSIONID 获取失败"
exit 1
else
echolog "当前 JSESSIONID 是 $JSESSIONID"
fi
post_data=$(cat "$GOT_FILE/loadbancedurl" \
| grep -i '<input' \
| grep -i 'type="hidden"' \
| sed -n 's/.*name="\([^"]*\)".*value="\?\([^">]\+\)"\?.*/\1=\2/p' \
| tr '\n' '&' | sed 's/&$//')
}
############################
# 5. 获取组播频道列表
############################
get_channel_list() {
#先进行验证直播源获取主要url为funcportalauth、frameset_judger
RESP=$(curl -s \
-A "$USERAGENT" \
-b "$JSESSIONID" \
-d "$post_data" \
-d "stbtype=B860AV1.1" \
"$AUTHHOST/iptvepg/function/funcportalauth.jsp" | tee $GOT_FILE/funcportalauth)
RESP=$(curl -s \
-A "$USERAGENT" \
-b "$JSESSIONID" \
-d "picturetype=1,3,5" \
"$AUTHHOST/iptvepg/function/frameset_judger.jsp" | tee $GOT_FILE/frameset_judger)
post_builder_data=$(cat "$GOT_FILE/frameset_judger" \
| grep -i '<input' \
| grep -i 'type="hidden"' \
| sed -n 's/.*name="\([^"]*\)".*value="\?\([^">]\+\)"\?.*/\1=\2/p' \
| tr '\n' '&' | sed 's/&$//')
echolog "[+] 获取组播频道列表..."
# 获取节目列表,下面参数不能少
PAGE=$(curl -s \
-b "$JSESSIONID" \
-A "$USERAGENT" \
-d "$post_builder_data" \
-d "hdmistatus=""" \
"$AUTHHOST/iptvepg/function/frameset_builder.jsp" | tee $GOT_FILE/iptvlist)
if [ -z "$PAGE" ]; then
echolog "[-] 未获取到频道列表"
exit 1
else
echolog "已获取到频道列表,网页保存在$GOT_FILE/iptvlist"
fi
TXT="$GOT_FILE/hitv.txt"
M3U="$GOT_FILE/hitv.m3u"
echo "#EXTM3U url-tvg=\"http://e.erw.cc/e.xml\"" > "$M3U"
echo "" > "$TXT"
echo "" > "$GOT_FILE/iptvlist.sort"
cat $GOT_FILE/iptvlist | grep -o 'ChannelID="[^"]*".*ChannelFccAgentAddr="[^"]*"' | \
while read -r line; do
CID=$(echo "$line" | sed -n 's/.*ChannelID="\([^"]*\)".*/\1/p')
NAME=$(echo "$line" | sed -n 's/.*ChannelName="\([^"]*\)".*/\1/p')
RTP=$(echo "$line" | sed -n 's/.*ChannelURL="igmp:\/\/\([^"]*\)".*/\1/p')
BACK=$(echo "$line" | sed -n 's/.*TimeShiftURL="\([^"]*\)".*/\1/p')
FFC=$(echo "$line" | sed -n 's/.*ChannelFCCServerAddr="\([^"]*\)".*/\1/p')
if echo "$NAME" | grep -iqE 'hd|4k'; then
# 进行时移链接转换
BACK=$(echo "$BACK" | sed "s#^rtsp://#${ROUTEIP}/rtsp/#")
# 写入临时排序文件:用 | 分隔字段
echo "${CID}|${NAME}|${RTP}|${BACK}|${FFC}" >> "$GOT_FILE/iptvlist.sort"
fi
done
sort -n -t'|' -k 1 "$GOT_FILE/iptvlist.sort" | while IFS='|' read -r CID NAME RTP BACK FFC; do
echo -e "${NAME},${ROUTEIP}/rtp/${RTP}?fcc=${FFC}\n${NAME},${BACK}" >> "$TXT"
echo "#EXTINF:-1 tvg-id="\"${CID}\"" tvg-name="\"${NAME}\"" catchup="\"default\"" catchup-source="\"${BACK}\&playseek={utc:YmdHMS\}-{utcend:YmdHMS}\"" tvg-logo="\"\"" group-title="\"ITV\"",${NAME}" >> "$M3U"
echo "${ROUTEIP}/rtp/${RTP}?fcc=${FFC}" >> "$M3U"
done
cp -f "$M3U" /www
cp -f "$TXT" /www
chmod 644 /www/hitv.m3u
chmod 644 /www/hitv.txt
echolog "IPTV 播放列表 生成完毕。$TXT $M3U"
}
############################
# 主流程
############################
mkdir -p "$GOT_FILE"
echo "" >> /usr/share/myfile/iptv.log
get_iptvip
get_ctc_token
make_authenticator
get_usertoken
get_session
get_channel_list该脚本只获取了含有HD和4K字样的频道名称,各地不一样注意修改。里面的 palyseek={utc:YmdHMS}-{utcend:YmdHMS} 及其他情况根据实际情况修改。将该段shell,utf-8编码,保存为 iptv.sh ,放到 /etc/init.d/ 文件夹内,给755 权限,shell终端执行/etc/init.d/iptv.sh即可自动生成IPTV M3U文件,还生成了文本格式cctv1,http://192.168.1.1/rtp/223... 。M3U文件放入到了/www/hitv.m3u,供其他终端获取,比如tivimate可以导入的M3U文件地址就是http://192.168.1.1/hitv.m3u。
还得加个hotplug 热插拔事件脚本,一旦iptv口重启或启动就自动生成新的m3u播放列表。重启后IPTV口的ip变了,rtsp单播鉴权信息就失效,需要重新生成。上一篇介绍过hotplug 热插拔事件脚本,稍微改下就可以用,就不重复发了。
几天折腾下来,终于圆满达成了目标。非专业人士,这2篇关于IPTV的文章如有错谬,敬请谅解。
技术不断升级,请注意文章时效性。
本站文章,欢迎转发。转载请注明出处:https://www.bandwh.com/net/2637.html

评论列表(2条)
我的ITV密码只有6位数,现在一直卡在获取USERTOKEN这一步,应该是加密过程有影响,还有MAC地址应该是输入大写的还是小写的,有啥好点的方法不
@明:解决了,补位那边就有