扒拉IPTV源的不完全指南
又是年初的时候,某股神秘力量影响着我去改动家里的网络架构,前年这个节点我弄来了小米路由器 AX3600
,去年弄来了J4125软路由
,今年弄来了(某环大陆产品):
当然了,了解这款产品不是一天两天了,至于为什么会选择今年拿下...🤔我也不太清楚。 关于这款产品的测评介绍网上已经够多了,所以感兴趣的自己搜搜看咯。
但因为这款产品还是有当作电视盒子
的需求在的,所以搞定电视频道的观看非常重要。
获得 IPTV 源
网络上有不少 IPTV 的现成解决方案,有的甚至集合了全世界范围数千个频道,也有那种有专人维护,比较稳定的付费源。
本来想着图省事要不直接用公开的源算了,转头想起之前家里办宽带送了一个 IPTV 盒子,迅速翻找出来、接网线、上电,别说,系统是垃圾了点,但是有我想要的 IPTV 资源在里面,开搞!
了解 IPTV 盒子的工作条件
经过一番了解,这个 IPTV 各家运营商都玩的挺花,有的还专门配发了VLAN
来保证 IPTV 的体验,你首先要清楚你手上的 IPTV 盒子是否有基于VLAN
、PPPoE
或者其他方案的授权,有这些情况大概率你需要准备东西来做转发,否则无法在第三方 IPTV 软件中播放。
本文的实现对象是是仅在获得流时需做一次性认证,所以算运气比较好😗,让我们继续吧!
抓包分析
首先我们得通过抓包来了解到 IPTV 盒子是如何获得直播流的。将盒子接入你所能控制的网络,这里你可以使用带抓包功能的路由,或者做网桥来实现,具体方法可以自行搜索。比如很好使的爱快自带的抓包工具:
将盒子关机后开始抓包,然后将盒子开机,随便播放几个频道,就可以停止抓包了,用 Wireshark 打开:
这么多包?我们直接找直播源,已知 IPTV 使用的流文件很大概率就是M3U
,这个文件基于HTTP
传输,所以直接过滤http
包 +m3u
字符串:
这么快找到一个,试着把这个 URL 直接在播放器中打开:
有效!那么接下来的工作就是往上翻阅请求,来看看这个流的 url 是怎么出来的。
这个就是我手头设备大概的请求流程,由于内容格式并不是 IPTV 播放器所能直接理解的订阅格式,所以需要弄个东西来代替 IPTV 盒子完成请求,并转换内容为 IPTV 播放器所能理解的文件内容。
获取并处理 IPTV 信息
经过测试,请求运营商的 IPTV 服务器的来源必须是本地运营商的 IP,所以我们需要在局域网内安排一个设备来实现请求转发。
我一开始使用的是内网穿透 + 云函数的方案(🤨后知后觉发现这么复杂完全是脱裤子放屁,可能我一开始打算在外网环境下观看吧...)
用 PHP 环境做了个简单的逻辑:
Copy<?php
$server = "内网穿透好了的URL";
$channel = explode("/channel/",$_SERVER["REQUEST_URI"],2);
header('Content-Type: application/vnd.apple.mpegurl');
header('Expires: 0');
if(count($channel) != 2){
exit(make_m3u());
}
get_stream($channel[1]);
function make_m3u(){
global $server;
$file = "#EXTM3U".PHP_EOL;
$endpoint = "获得频道列表";
$params = [
认证信息
];
$list = file_get_contents($server.$endpoint."?".http_build_query($params));
$list = json_decode($list,true);
if($list['result']['reason'] != 'ok')
return 'fail';
$list = $list['channelList'];
foreach($list as $channel){
$file.= "#EXTINF:-1 ";
if (isset($channel['callsign']))
$file.="tvg-logo=\"".$channel['callsign']."\", ";
$file.= $channel['channelName'];
$file.= PHP_EOL."解析频道信息URL".$channel['channelId'].PHP_EOL;
}
return $file;
}
function get_stream($channel){
global $server;
$endpoint = "获得直播源";
$params = [
鉴权及频道信息
];
$ch = curl_init($server.$endpoint."?".http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-Forwarded-IP: '.$_SERVER['HTTP_X_FORWARDED_FOR']
]);
$response = curl_exec($ch);
curl_close($ch);
$stream = json_decode($response,true);
if($stream['result']['reason'] != 'ok')
return 'fail';
header('Location: ' . $stream['playAddress'], true, 302);
}
?>
能正常解析并获得视频流。
但是我发现,这段请求在公网上绕了一圈,从家里回到家里... 速度太慢了,而且弯弯绕绕极其不优雅。
但我现在基本对家里供台服务器没什么兴趣了(可能已经退烧了),所以部署难度及复杂度上能低就低,所以继续动手优化这一过程。
先提一嘴反向代理遇到的坑
鉴权后返回的流 URL 是会带有一个userIp
参数,这个参数经过实验,是获取自HTTP_X_FORWARDED
这个HTTP 头
,很不巧的是,这个头会被一些 CDN / 转发服务 / Web 服务器自动地加上访问者的 IP,本来是用于最终目的地好区分访问者真实 IP 的,结果到了鉴权这里就成坏事了。
要解决也很简单,我是用Caddy
来实现反向代理,所以只需要加入一行配置:
header_up X-Forwarded-For {http.request.header.Fake-IP}
当然这里的名字你可以自己定义,在请求的时候携带Fake-IP
这个头即可,避免了请求路径中有东西擅自给你加 IP😂
使用 Go 来实现处理的部分
直接上代码吧:
Copypackage main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strings"
)
const sourceServer = "视频流服务器"
const listServer = "频道列表服务器"
func main() {
http.HandleFunc("/", getList)
http.HandleFunc("/ch/", getChannel)
log.Fatal(http.ListenAndServe(":80", nil))
}
func getList(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
var list []interface{}
var file strings.Builder
file.WriteString("#EXTM3U\n")
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
w.Header().Set("Expires", "0")
params := url.Values{
认证信息
}
resp, err := http.Get(listServer + "?" + params.Encode())
if err != nil {
fmt.Fprint(w, err)
return
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&data)
if data["result"].(map[string]interface{})["reason"].(string) != "ok" {
fmt.Fprint(w, "fail")
}
list = data["channelList"].([]interface{})
for _, channel := range list {
file.WriteString("#EXTINF:-1")
if channel.(map[string]interface{})["channelName"] == nil {
continue
}
if channel.(map[string]interface{})["callsign"] != nil {
file.WriteString(" tvg-logo=\"" + channel.(map[string]interface{})["callsign"].(string) + "\"")
}
file.WriteString("," + channel.(map[string]interface{})["channelName"].(string) + "\n")
file.WriteString("\nhttp://运行程序终端的IP/ch/" + channel.(map[string]interface{})["channelId"].(string) + "\n")
}
fmt.Fprint(w, file.String())
}
func getChannel(w http.ResponseWriter, r *http.Request) {
channel := strings.TrimPrefix(r.URL.Path, "/ch/")
var data map[string]interface{}
params := url.Values{
认证信息
}
resp, err := http.Get(sourceServer + "?" + params.Encode())
if err != nil {
fmt.Fprint(w, err)
return
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&data)
if data["result"].(map[string]interface{})["reason"].(string) != "ok" {
fmt.Fprint(w, "fail")
}
http.Redirect(w, r, data["playAddress"].(string), http.StatusFound)
}
这样,只需要把程序塞进一个简单的 Linux 环境内(我是直接丢 Docker 了),就可以让软路由运行转换服务器。
享受你的电视!
文章来源:
Author:时光的时光轴
link:https://xlog.app/api/redirection?characterId=51408¬eId=47