扒拉IPTV源的不完全指南

又是年初的时候,某股神秘力量影响着我去改动家里的网络架构,前年这个节点我弄来了小米路由器 AX3600,去年弄来了J4125软路由,今年弄来了(某环大陆产品):

某环大陆产品

当然了,了解这款产品不是一天两天了,至于为什么会选择今年拿下...🤔我也不太清楚。 关于这款产品的测评介绍网上已经够多了,所以感兴趣的自己搜搜看咯。

但因为这款产品还是有当作电视盒子的需求在的,所以搞定电视频道的观看非常重要。

获得 IPTV 源

网络上有不少 IPTV 的现成解决方案,有的甚至集合了全世界范围数千个频道,也有那种有专人维护,比较稳定的付费源。

本来想着图省事要不直接用公开的源算了,转头想起之前家里办宽带送了一个 IPTV 盒子,迅速翻找出来、接网线、上电,别说,系统是垃圾了点,但是有我想要的 IPTV 资源在里面,开搞!

了解 IPTV 盒子的工作条件

经过一番了解,这个 IPTV 各家运营商都玩的挺花,有的还专门配发了VLAN来保证 IPTV 的体验,你首先要清楚你手上的 IPTV 盒子是否有基于VLANPPPoE或者其他方案的授权,有这些情况大概率你需要准备东西来做转发,否则无法在第三方 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来实现反向代理,所以只需要加入一行配置:

Copy
header_up X-Forwarded-For {http.request.header.Fake-IP}

当然这里的名字你可以自己定义,在请求的时候携带Fake-IP这个头即可,避免了请求路径中有东西擅自给你加 IP😂

使用 Go 来实现处理的部分

直接上代码吧:

Copy
package 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