家庭iPTV改造

目前的家庭看iPTV已经处于一个很尴尬的地步,食之无味,弃之可惜。家里不备用一个iPTV吧,某个时间如果需要它就很尴尬,弄了吧,又需要极其麻烦的线路走线,影响电视墙美观,还占用端口,倘若老人房需要再加一台设备,还得加钱加设备。说实话,就电信送的机顶盒是真心的又丑又卡,广告繁多,为此,摆脱设备限制,实现机顶盒所有扩能,并进一步扩展观看方式成了一个伪命题下的刚需。

20260117更新

网上有很多网友希望我分享一下自动更新频道数据的脚本,在此我做一个分享吧。所需要的token需要在以下手动步骤中抓取频道数据的http请求中,目前我这个token已经使用了2年左右,并且是我老家的iptv抓取的,和我这条宽带都没有关系。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# -*- coding: utf-8 -*-
import requests
import re
import os
import time
import logging

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

DOMAIN = 'http://118.122.163.150:33200' # 这个ip地址需要看你所有请求的ip地址是多少
AUTHENTICATOR = 'xxxx' # 这就是前面所说的token


class Client(object):
    def __init__(self, iptv_account, rtsp_file_dir=None):
        if rtsp_file_dir is None:
            rtsp_file_dir = os.path.join(os.path.dirname(__file), 'result')
        self.iptv_account = iptv_account
        self.user_token = None
        self.session = requests.Session()
        self.rtsp_file_dir = rtsp_file_dir

    def pre_login(self):
        url = f'{DOMAIN}/EPG/jsp/authLoginHWCTC.jsp'
        payload = f'UserID={self.iptv_account}&VIP='
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}

        try:
            response = self.session.post(url, headers=headers, data=payload)
            response.raise_for_status()
            logging.info('Pre-login response: %s', response.text)
        except requests.RequestException as e:
            logging.error('Pre-login failed: %s', e)

    def login(self):
        url = f"{DOMAIN}/EPG/jsp/ValidAuthenticationHWCTC.jsp"
        payload = f'UserID={self.iptv_account}&Lang=0&SupportHD=1&NetUserID=&Authenticator={AUTHENTICATOR}'
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}

        try:
            response = self.session.post(url, headers=headers, data=payload)
            response.raise_for_status()
            self.user_token = re.search(r'name="UserToken" value="(.*?)"', response.text).group(1)
            logging.info('User token obtained: %s', self.user_token)
        except (requests.RequestException, AttributeError) as e:
            logging.error('Login failed: %s', e)

    def get_channel(self):
        url = f"{DOMAIN}/EPG/jsp/getchannellistHWCTC.jsp"
        payload = f'conntype=dhcp&UserToken={self.user_token}&UserID={self.iptv_account}&Lang=1'
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}

        while True:
            try:
                response = self.session.post(url, headers=headers, data=payload)
                response.raise_for_status()
                if 'ChannelID' in response.text:
                    logging.info('Channel list obtained successfully')
                    break
                else:
                    logging.warning('Failed to obtain channel list, retrying...')
                    time.sleep(30)
            except requests.RequestException as e:
                logging.error('Error obtaining channel list: %s', e)
                time.sleep(30)

        self.create_rtsp_file(response.text)

    def create_rtsp_file(self, content):
        print(content)
        if not os.path.exists(self.rtsp_file_dir):
            os.makedirs(self.rtsp_file_dir)
        
        rtsp_raw_file_path = os.path.join(self.rtsp_file_dir, f'iptv_rtsp_raw.m3u8')
        fcc_file_path = os.path.join(self.rtsp_file_dir, f'iptv_fcc.m3u8')  # FCC格式文件

        # 使用正则表达式匹配完整的频道块
        channel_blocks = re.findall(r"Authentication\.CTCSetConfig\('Channel','(.*?)'\)", content)
        
        # 原始RTSP格式
        m3u8_content = "#EXTM3U\n"
        # FCC格式
        fcc_m3u8_content = "#EXTM3U\n"
        
        for block in channel_blocks:
            # 从每个频道块中提取所需信息
            channel_name_match = re.search(r'ChannelName="(.*?)"', block)
            channel_url_match = re.search(r'TimeShiftURL="(.*?)"', block)
            fcc_ip_match = re.search(r'ChannelFCCIP="(.*?)"', block)
            fcc_port_match = re.search(r'ChannelFCCPort="(.*?)"', block)
            
            if channel_name_match and channel_url_match:
                channel_name = channel_name_match.group(1)
                channel_url = channel_url_match.group(1)
                fcc_ip = fcc_ip_match.group(1) if fcc_ip_match else ""
                fcc_port = fcc_port_match.group(1) if fcc_port_match else ""
                
                if '画中画' in channel_name or '单音轨' in channel_name:
                    continue
                    
                group_title = '其他'
                if 'CCTV' in channel_name:
                    group_title = '央视'
                elif '卫视' in channel_name:
                    group_title = '卫视'
                elif '专区' in channel_name:
                    group_title = '专区'
                elif '直播室' in channel_name:
                    group_title = '直播室'
                
                # 原始RTSP格式
                m3u8_content += f"#EXTINF:-1 tvg-id=\"{channel_name}\" tvg-name=\"{channel_name}\" group-title=\"{group_title}\", {channel_name}\n"
                m3u8_content += f"{channel_url.split('?')[0]}\n"
                
                # FCC格式 - 所有频道都保留,有FCC信息的频道添加fcc参数
                # 获取RTSP地址的基础部分(去掉参数)
                base_rtsp_url = channel_url.split('?')[0]
                
                # 如果有FCC信息,添加fcc参数
                if fcc_ip and fcc_port:
                    fcc_url = f"{base_rtsp_url}?fcc={fcc_ip}:{fcc_port}"
                    # 添加catchup参数
                    fcc_m3u8_content += f'#EXTINF:-1 tvg-id="{channel_name}" tvg-name="{channel_name}" group-title="{group_title}" catchup="default" catchup-source="{base_rtsp_url}?playseek={{utc:YmdHMS}}-{{utcend:YmdHMS}}", {channel_name} [FCC]\n'
                    fcc_m3u8_content += f"{fcc_url}\n"
                else:
                    # 没有FCC信息,使用原始RTSP地址,也添加catchup参数
                    fcc_m3u8_content += f'#EXTINF:-1 tvg-id="{channel_name}" tvg-name="{channel_name}" group-title="{group_title}" catchup="default" catchup-source="{base_rtsp_url}?playseek={{utc:YmdHMS}}-{{utcend:YmdHMS}}", {channel_name}\n'
                    fcc_m3u8_content += f"{base_rtsp_url}\n"
        
        # 写入原始RTSP文件
        with open(rtsp_raw_file_path, 'w', encoding='utf8') as f:
            f.write(m3u8_content)
        logging.info('RTSP file created at %s', rtsp_raw_file_path)
        
        # 写入FCC文件
        with open(fcc_file_path, 'w', encoding='utf8') as f:
            f.write(fcc_m3u8_content)
        logging.info('FCC file created at %s', fcc_file_path)

if __name__ == '__main__':
    iptv_account = 'xxxx' # 你的iptv账号
    client = Client(iptv_account, rtsp_file_dir="/www/")   # 保存在www主要是可以直接访问openwrt的地址就可以完美获取,不再另设文件服务器
    client.pre_login()
    client.login()
    client.get_channel()

以下为手动操作教程。

我忘记了截图获取token的步骤,请大家自行看请求头!!!!! 我忘记了截图获取token的步骤,请大家自行看请求头!!!!! 我忘记了截图获取token的步骤,请大家自行看请求头!!!!!


1. 梳理网络架构

想要实现改造,先需要理清网络架构,当前由于家里还未搬入新房,目前的主路由设备不支持VLAN,所以线路稍微还是有点复杂的。直接看架构图吧。

家庭iPTV改造.png

为了不影响家庭主要网络,进一步提高主路由设备的功效,毕竟它是带有路由芯片及背板的,还是采用了旁路由模式,这就导致网络稍微有点复杂。总的来说就是有以下几点:

  1. 光猫需要改桥接(后文会讲述如何获取超密改桥接,或者直接打电话给10000号远程改桥接)
  2. 主路由负责拨号。将光猫网络口主路由wan
  3. 旁路由负责科学,去广告(主路由lan旁路由lan);直播信号组播转单拨,单拨地址回看(光猫iTV旁路由的另一条lan)。所以,不使用VLAN的方式,旁路由至少得是双口的机器。

2. 获取机顶盒鉴权以及电视节目地址

理论上来说,四川电信已经不需要抓包鉴权了,直接看机顶盒背面的硬件信息已经可以完美实现鉴权了。但是还是记录下两种鉴权的方式吧。

2.1 抓包鉴权及获取电视节目地址

抓包鉴权有几种方式,比较方便的是用一台openwrt路由器作为交换机,使用tcpdump抓包,但是我这没有多余设备了,如果使用该方式就需要改变现有网络架构,所以还是采取了购买抓包神奇的方式实现,毕竟成本20块钱以内,申请下经费也很好批。这东西长下面这样。

家庭iPTV改造-50.png

完全不需要插电的小玩意,用起来很方便,直接插网线就行,下面说以下接线方式。

家庭iPTV改造-51.png

如图所示,抓包的时候有一些小细节,电脑如果接在了靠光猫一侧,获取的包为iTV发出的,接在了机顶盒一侧,获取的包为机顶盒发出的,所以这就导致了没法获取一个完整的响应。根据自身测试,建议还是把电脑接在光猫的iTV口一侧,毕竟我们更需要的是光猫返回的数据,至于机顶盒的请求数据一般在光猫返回的数据里会负载,由于我不打算完全逆向机顶盒运行逻辑,更多的数据也是没必要。

按图示接好所有线路后,电脑打开wireshark启动好监听,监听的网口选择接网线的接口,然后上电机顶盒,等到机顶盒正常运行即可。然后搜索dhcp,主要找dhcp request,找不到也无所谓,考虑其他的跟dhcp返回请求有关的也行,然后查看option12,option61(没有的话直接看请求头,就是找设备的mac),option 60,留存备用。此处我以其他图示意一下。

家庭iPTV改造-52.png

然后继续找http的内容,这里忘记截图了,主要内容是找响应中有类似如图中的数据。其中igmp开头的为组播地址,rtsp为单拨地址,拷贝下来备用。

家庭iPTV改造-53.png

到这里,机顶盒完成了它的所有使命,全部所需的数据已经拿到。

2.2 直接看设备鉴权

把你的机顶盒翻个面,后面印刷了该设备的设备名、mac地址,完事。只是相对前面,缺少了直播信息。(需要直播信息请查看上一步)

3. 旁路由模拟机顶盒鉴权

由于我使用的openwrt是ImmortalWrt 23.05.4分支,部分网络上的教程已经和这里的界面不一样了,请慎重参考。

3.1 修改mac地址

如下图操作界面,直接把插光猫itv的网口的mac地址修改为刚才抓包到的mac地址,保存即可。

家庭iPTV改造-54.png

3.2 新建iptv网口

新建一个网口,如下图:

家庭iPTV改造-55.png

配置参数(我已经在使用了,所以就用这个图参考)。主机名就是option12的值,否则机顶盒背面的设备编号。

家庭iPTV改造-56.png

供应商类别是option60的值。默认网关打开,自动获取DNS服务器,跃点大一点最好。

家庭iPTV改造-57.png

然后保存应用即可,到这里就完成了鉴权,网口会出现以下信息,有ip出现表示鉴权成功。

家庭iPTV改造-58.png

4. 配置组播转换

根据个人习惯,我选择的组播转换插件为msd,配置全部默认,接口选择iptv即可。

家庭iPTV改造-59.png

5. 生成组播转单播

根据刚才提到的直播信息,把它提取为一个播放器可以播放的文件,有毅力的人可以手动,没毅力的人让chatgpt给你写个代码吧。分享下我的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import re

scripts = '此处填入抓取的地址信息'
  
ROUTERURL = '10.0.10.200'  ## 我的旁路由地址
RROUTETEPORT = '7088'  ## msd的端口

m3u8_content = "#EXTM3U\n"
channel_name_matches = re.findall(r'ChannelName="(.*?)"', scripts)
channel_url_matches = re.findall(r'ChannelURL="(.*?)"', scripts)

for channel_name, channel_url in zip(channel_name_matches, channel_url_matches):
    if '画中画' in channel_name or '单音轨' in channel_name:
        continue
    m3u8_content += f"#EXTINF:-1 tvg-id=\"{channel_name}\" tvg-name=\"{channel_name}\" group-title=\"Live TV\", {channel_name}\n"
    m3u8_content += f"http://{ROUTERURL}:{RROUTETEPORT}/udp/{channel_url.replace('igmp://', '')}\n"

with open('./iptv.m3u8', 'w', encoding='utf8') as f:
    f.write(m3u8_content)

无论采取什么办法,保证生成的直播连接地址类似于http://msd插件所在的ip:msd端口/udp/igmp地址的信息,样例http://10.0.10.200:7088/udp/239.93.2.58:5140

6. 测试播放

将生成的iptv.m3u8文件使用VLC播放。

家庭iPTV改造-60.png


将这个文件放进电视的任何地址即可实现播放,基本教程完结。后面则是进阶教程。

7. 回看地址生成

根据刚才提到的直播信息,提取rstp信息即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import re

scripts = '此处填入抓取的地址信息'
m3u8_content = "#EXTM3U\n"

channel_name_matches = re.findall(r'ChannelName="(.*?)"', scripts)
channel_url_matches = re.findall(r'TimeShiftURL="(.*?)"', scripts)

for channel_name, channel_url in zip(channel_name_matches, channel_url_matches):
    if '画中画' in channel_name or '单音轨' in channel_name:
        continue
    m3u8_content += f"#EXTINF:-1 tvg-id=\"{channel_name}\" tvg-name=\"{channel_name}\", {channel_name}\n"
    m3u8_content += f"{channel_url.split('?')[0].replace('rtsp', 'http')}/index.m3u8?fmt=ts2hls\n"

with open('./iptv_rtsp.m3u8', 'w', encoding='utf8') as f:
    f.write(m3u8_content)

然后就生成了可回看的地址 ,回看信息使用网上的EPG服务器即可。

9. 地址转发

回看地址生成其实很简单,但是这个ip地址内外是不通的,只能经过iptv的接口过去,所以需要配置静态路由。

家庭iPTV改造-61.png

目标为iptv回看地址的网段,网关需要自行根据iptv接口的ip和掩码反算,配置好即可。

额外教程:电信光猫超密获取

直接采用https://cloud.tencent.com/developer/article/1921562

1. 普通用户登录

用户名和密码在光猫的背面。登录地址是,192.168.1.1:8080,用户名一般是 useradmin。

2. 进入设备管理

一定要确认进入设备管理界面,这个界面连 USB 都给屏蔽了。

3. 查找 sessionKey

按 F12(或者右键→检查),找到框起来的部分。

家庭iPTV改造-63.png

4. 构造命令执行

location.assign("/usbbackup.cmd?action=backupeble&set2_sessionKey=set2_sessionKey_304") 然后回车

打开页面后,右键备份配置,点检查,删除 disable,现在可以点击 "备份配置" 正常备份了,点击之后页面会全白,那是正常的。

5. 得到备份文件

U 盘上会生成一个名为 e8_Config_Backup 的文件夹,其下的 ctce8_TEWA-708G.cfg 就是路由器配置文件。

6. 解密文件

如果用记事本打开 ctce8_TEWA-708G.cfg,会是一堆乱码。要使用 xor 或者 router pass view 打开配置文件,查找 TeleComAccount,下面的 Password 内即为超级密码。 xor 下载 https://github.com/jonirrings/xor cmd内执行xor cfg文件会在同目录下得到 ctce8_TEWA-708G.cfg.xml 文件。

7.获取超密

打开 ctce8_TEWA-708G.cfg.xml 按 Ctrl+F 查找 telecomadmin 后面带数字的就是超级密码。 账号:telecomadmin 密码:telecomadminxxx

8. 关闭启用周期上报

进入 Web 后台管理界面后,切换到 "网络"-"远程管理" 选项卡,清除勾选 "启用周期上报" 项,这样就彻底阻止了电信对光猫的远程控制功能。

加载评论