IPTV电视直播源管理系统:自动检测、格式转换与多设备分发
简介IPTV电视直播源管理系统源码是一套面向定制直播软件需求的完整后台管理项目主要服务于需要对接DIYP影音播放器的开发者与技术爱好者作者因现有方案操作不便参考恩山无线论坛相关思路后自行编写接口与后台项目不内置直播源需自行准备。资源包共198个文件约20.87MB包含Python源码、HTML/CSS/JS前端页面、图片素材、说明文档及一个DIYP修改版APK其中Python源码为Django后台核心逻辑前端文件支撑管理界面展示APK可用于参考播放器与后台的对接方式APK与readmeimg目录并非项目必需文件部署时可忽略。目前已有830人学习下载通过源码可快速搭建一套轻量级IPTV后台学习Django框架在直播源管理中的实际应用并对照APK理解定制化电视直播软件的实现路径适合个人开发者用于学习研究或作为二次开发基础。1. IPTV电视直播源管理系统从散乱源列表到可控分发做IPTV直播源管理最头疼的不是找不到源而是源太多之后没人记得住谁还能用。今天拆的这套IPTV电视直播源管理系统源码核心就是把这摊散乱的直播源URL收拢成一套带频道分组、自动检测、EPG节目单和多设备分发的管理后端。它解决的是三个具体问题源失效没人知道、频道排序靠手工、不同设备拿到的地址不统一。适合三类人家里多台设备看IPTV想统一入口的软路由加udpxy玩单线复用的以及自建直播源分发服务的运维。这套系统落地之后直播源有效性检测、M3U/TXT格式转换、按设备维度下发频道列表这些动作就都能在后台完成不用再靠记事本手动维护。下面按我从源码里拆出来的实际结构从数据层到部署排查逐层说。2. 核心模块与数据模型先看频道和源是怎么组织起来的2.1 四个核心模块频道、源、EPG、设备一套直播源管理系统如果只有一张源地址表那跟Excel没区别。这套源码里实际划分了四个模块各自管一块独立职责。频道模块管的是“观众看到的东西”频道名、台标logo、所属分组央视/卫视/地方台、排序权重。直播源模块管的是“实际播放的地址”每个频道可以挂多个源按优先级排列带协议类型rtmp/hls/m3u8/rtsp、状态、最近检测时间、响应耗时。EPG模块管节目单频道ID关联节目开始时间、结束时间、节目名称。设备模块管分发每台设备一个token绑定允许访问的分组后端按token生成对应的播放列表。这四个模块的关系是频道是主表源和EPG都挂在频道底下设备只跟分组打交道。好处是换源的时候不用动设备配置检测到旧源失效、新源可用时后端自动切换设备侧的播放列表URL不变。2.2 数据表设计五张表把关系理清从源码的SQL初始化文件里能看到完整的建表语句核心是五张表。我重新整理过字段去掉冗余之后的结构是这样CREATE TABLE channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, logo TEXT, group_name TEXT DEFAULT 未分组, sort_order INTEGER DEFAULT 0, status INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE streams ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id INTEGER NOT NULL, url TEXT NOT NULL, protocol TEXT DEFAULT hls, priority INTEGER DEFAULT 0, status INTEGER DEFAULT 1, response_time_ms INTEGER DEFAULT 0, last_check_at DATETIME, FOREIGN KEY (channel_id) REFERENCES channels(id) ); CREATE TABLE epg ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id INTEGER NOT NULL, program_name TEXT, start_time DATETIME, end_time DATETIME, FOREIGN KEY (channel_id) REFERENCES channels(id) ); CREATE TABLE devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_name TEXT, token TEXT UNIQUE NOT NULL, allowed_groups TEXT DEFAULT *, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE config ( key TEXT PRIMARY KEY, value TEXT );这里需要注意几个字段的设计意图。streams.protocol不只是存一个类型名它决定了播放器用什么方式拉流m3u8和rtsp的播放逻辑完全不同。priority字段用于多源自动切换数值越小优先级越高。devices.allowed_groups用*表示全部放行也可以写成央视,卫视这种逗号分隔的分组名源码里解析这个字段时是按逗号切分的。config表存的是key-value形式的运行时配置比如检测间隔、超时时间、备份路径。这套系统把配置落库而不是写死在代码里好处是改参数不用重启服务坏处是有的人会忘了改过什么这属于取舍问题。2.3 配置项哪些参数值得一改配置文件里默认给了一套参数我挑几个实际部署时一定会改的说一下配置键默认值作用建议check_interval3600源有效性检测间隔秒直播源稳定性差就改1800稳定源可以拉到7200check_timeout_ms5000单次检测超时毫秒内网源3000就够公网源建议8000以上check_concurrency10并发检测数量源量大时调高但注意别把上游拉爆epg_offset_hours0EPG时区偏移小时节目单对不上时间时用这个调backup_enabled1是否启用自动备份默认开着别关backup_dir./backup备份目录放到挂载盘上防止容器删了数据全没3. 直播源接入与格式转换M3U解析、有效性检测、URL标准化3.1 M3U与TXT格式解析不同来源的源地址统一入库搞IPTV直播源的人手里最多的就是两种格式m3u文件带#EXTINF信息和纯文本格式一行一个URL。源码里做了两个解析器核心逻辑拆出来是这样一个流程import re from urllib.parse import urlparse def parse_m3u(content): 解析M3U格式直播源返回频道名和URL的列表 channels [] lines content.splitlines() current_name None current_logo None current_group None for line in lines: line line.strip() if line.startswith(#EXTINF:): # 提取频道名、台标、分组信息 logo_match re.search(rtvg-logo([^]), line) group_match re.search(rgroup-title([^]), line) current_logo logo_match.group(1) if logo_match else None current_group group_match.group(1) if group_match else 未分组 # 频道名在最后一个逗号后面 current_name line.split(,)[-1].strip() if , in line else None elif line and not line.startswith(#): # 这一行是URL跟前面的元信息配对 channels.append({ name: current_name or 未命名频道, url: line, logo: current_logo, group: current_group }) current_name None return channels def normalize_url(url): URL标准化处理常见的不规范写法 url url.strip() if in url: url url.replace( , %20) # 有些源地址带引号包裹需要剥掉 if url.startswith() and url.endswith(): url url[1:-1] return url我这边实际跑下来有几个注意点。#EXTINF这一行不同来源的格式差异很大有的带tvg-id有的带tvg-name有的什么都不带只有频道名。上面的正则只抓了tvg-logo和group-title两个最常用的属性够用但不算全。如果你想连节目单的tvg-id一起解析在正则里加一行id_match re.search(rtvg-id([^]*), line)就行。3.2 抓包获取真实播放地址从运营商软终端里拿源很多运营商的IPTV源不是直接给公网地址的装维给你的是一个软终端APP点开就能看。这背后的真实播放地址需要通过抓包拿到。这个操作不算破解属于常规的流量分析。我常用的方式是在软路由或电脑上开抓包工具过滤条件直接用http或rtsp然后打开软终端切几个频道。播放请求里会出现类似http://192.168.x.x:8000/PLTV/88888888/.../index.m3u8或者rtsp://192.168.x.x:554/...的地址这就是真实播放源。拿到地址之后先用ffprobe验证一下能不能播ffprobe -v error -show_entries formaturl -show_entries streamcodec_type,width,height \ -user_agent IPTV-Player -i http://192.168.x.x:8000/PLTV/88888888/.../index.m3u8 -show_streams如果输出里能看到codec_typevideo且width是1920或1280说明源是通的。这里有个坑很多运营商的源地址带IP防盗链直接用播放器打开可能403但带对User-Agent就能过。上面命令里-user_agent IPTV-Player就是干这个的具体UA值要看你抓包时软终端发的是什么。3.3 有效性检测并发探测与自动下线源管理系统的核心价值之一就是自动检测源能不能用。源码里这个模块是用协程做的并发检测逻辑非常直接import asyncio import aiohttp from urllib.parse import urlparse async def check_stream(session, url, timeout_ms5000, min_content_length512): 检测单个直播源是否可用 返回 (status: bool, response_time_ms: int) parsed urlparse(url) if parsed.scheme rtsp: # RTSP协议用常规HTTP方式探不通跳过网络检测直接标记为待人工确认 return True, 0 start asyncio.get_event_loop().time() try: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: http://example.com/ } # 只拉取一小段字节避免流量浪费 async with session.get(url, timeoutaiohttp.ClientTimeout(totaltimeout_ms / 1000), headersheaders) as resp: if resp.status ! 200: return False, int((asyncio.get_event_loop().time() - start) * 1000) # 读取一小段数据验证不是空流 chunk await resp.content.read(min_content_length) if len(chunk) min_content_length: return False, int((asyncio.get_event_loop().time() - start) * 1000) return True, int((asyncio.get_event_loop().time() - start) * 1000) except Exception: return False, int((asyncio.get_event_loop().time() - start) * 1000) async def run_checks(streams, concurrency10, timeout_ms5000): 并发执行批量检测 results {} semaphore asyncio.Semaphore(concurrency) async def guarded(stream): async with semaphore: status, rt await check_stream(session, stream[url], timeout_ms) results[stream[id]] (status, rt) async with aiohttp.ClientSession() as session: tasks [asyncio.create_task(guarded(s)) for s in streams] await asyncio.gather(*tasks) return results两个参数值得细说。min_content_length512是防止那种“HTTP 200但其实是空流”的假活源有些失效源返回200但响应体为空或只有几字节。User-Agent和Referer是防盗链最常见的绕过手段实际使用时要改成你自己抓包拿到的真实值。另外这个检测方案基本不适用于RTSP源——RTSP有单独的信令流程HTTP探活会把RTSP源误判为失效。4. 部署方式与常见问题排查Docker部署后容易翻车的五个地方4.1 Docker部署一条compose拉起来源码里带了一个Dockerfile构建起来的镜像可以直接跑。我最常用的部署方式是docker-compose配置非常简单version: 3.8 services: iptv-manager: image: iptv-manager:latest container_name: iptv-manager restart: unless-stopped network_mode: host volumes: - ./data:/app/data - ./backup:/app/backup - /etc/localtime:/etc/localtime:ro environment: - TZAsia/Shanghai - DB_PATH/app/data/iptv.db - PORT8080network_mode: host这一步很重要。如果你家里有组播源要通过udpxy转单播或者直播源在运营商内网需要走指定VLANhost网络模式是最省事的。用bridge模式的话容器里访问组播地址大概率不通排查起来还特别隐蔽。不过host模式也有代价——容器的端口直接暴露在宿主上如果宿主本身有防火墙策略需要确认端口放行。启动之后访问http://你的IP:8080进后台先创建设备token然后导入M3U文件。导入流程在界面上是“设置 → 源管理 → 导入”导入后系统会自动跑第一轮检测。4.2 常见问题排查五个高频翻车点下面这五条是我在实机部署里踩过或者看别人踩过的坑按现象 → 原因 → 解决的顺序写。翻车点一源检测显示全部失效但播放器明明能播现象后台批量检测之后所有源都是红色失效状态但用VLC直接打开同一个URL能正常播放。原因检测脚本请求时带的User-Agent和播放器不一样运营商的源对UA做了白名单限制非白名单UA直接拒绝。解决先用抓包工具看真实播放请求的UA是什么然后到源码的检测模块里把User-Agent改成一致的值。改完重启检测状态就会恢复。翻车点二组播地址在Docker容器里检测不通现象源地址是rtp://239.0.0.1:1234这种组播地址宿主机上播着正常容器里检测永远超时。原因bridge网络模式下容器的网络命名空间独立组播包进不到容器里。解决要么把compose文件的network_mode改成host要么用udpxy把组播转成单播地址再填入系统。我一般推荐后者因为组播地址只要不经过udpxy转发手机在外网根本没法看。翻车点三EPG节目单整体错位差刚好一个小时现象下午2点的节目显示到下午3点所有频道统一偏移。原因默认EPG源是UTC时间数据库里存的也是UTC但PHP/Node.js后端输出到页面时用的是服务器本地时间两边没对齐。解决在config表里把epg_offset_hours配成8或-8看实际偏移方向调。调完之后清缓存再看。翻车点四M3U导入后频道名全是乱码现象导入M3U文件之后后台频道名显示成æ±äº¬å°è§这种。原因M3U文件是GBK编码源码默认按UTF-8读取字节被错误解码。解决导入前在命令行先转码iconv -f gbk -t utf-8 source.m3u source_utf8.m3u然后再导入UTF-8版本。这个问题在运营商给的旧格式文件里特别常见新版的M3U8基本都是UTF-8但防不住手头有老文件。翻车点五URL里带和导致播放失败现象同一个源地址后台测通播放器播放黑屏或报403。原因URL参数被解析成HTML实体或被后端框架二次编码导致播放器请求的地址和真实地址不一致。解决在源地址入库前做一次标准化把amp;还原成并且确认前端输出播放链接时用的编码函数是urlencode而不是htmlspecialchars。这个坑在PHP写的系统里尤其常见。5. 进阶用法API对接、健康检查策略与自动化运维5.1 对外API让播放器直接拿订阅列表这套系统带一组只读API核心用途是给播放器客户端提供订阅列表。以Kodi、TiviMate这类播放器为例它们都支持通过URL订阅频道列表。接口设计大致是GET /api/channels - 返回全部分组和频道列表 GET /api/playlist.m3u?tokenxxx - 按设备token返回M3U播放列表 GET /api/streams/{id}/status - 查询单个源的健康状态 GET /api/health - 系统健康检查/api/playlist.m3u的返回值是按设备维度过滤过的——设备只拿到自己被允许的分组。这个设计的实际意义是给父母的电视盒子只开放央视和地方卫视不开放付费体育频道给自己的手机全放开。播放器侧的更新频率各不一样TiviMate默认是按需刷新这个接口重复调用没有副作用。5.2 健康检查策略别用固定间隔硬扫加个退避固定间隔扫源最大的问题是一视同仁。一个稳定运行半年的源和一个刚加的源用同一个检测频率浪费带宽不说还容易在源抖动的时候误切换。我一般会在源码的检测逻辑上改进对连续失败的源逐步降低检测频率。import time def next_check_interval(consecutive_failures): 根据连续失败次数动态计算下一次检测间隔秒 base_interval 300 # 正常状态下每5分钟扫一次 max_interval 7200 # 最多拉到2小时扫一次 backoff_factor 2 if consecutive_failures 0: return base_interval # 连续失败次数越多扫描间隔越长避免对失效源频繁发无效请求 interval base_interval * (backoff_factor ** min(consecutive_failures, 5)) return min(interval, max_interval) def should_resurrect(failure_count): 失败太多次的源不再自动恢复标记为需人工确认 return failure_count 10这个思路的本质是失效源不用抢检测资源活着的源才需要监控。系统里再加一个“复活检测”队列每隔4小时对失效源做一次低并发扫描恢复了的自动重新上线。5.3 与Home Assistant联动一个让IPTV源“会说话”的小技巧设备token这个设计可以跟家庭自动化联动。我在Home Assistant里加了一个脚本每天凌晨3点调一次/api/health接口如果连续3次返回失败就触发一个通知到手机。automation: - alias: IPTV系统健康检查 trigger: platform: time at: 03:00:00 action: - service: rest_command.iptv_health - delay: 00:00:10 - condition: template value_template: {{ states.rest_command.iptv_health.attributes[status] ! 200 }} - service: notify.mobile_app_phone data: message: IPTV源管理系统异常请检查服务状态这个联动逻辑本身很简单但它利用了一个容易被忽略的点系统自身的健康状态和源的健康状态是分开的系统是Docker容器、源是运营商链路两边都可能挂需要分别监控。我自己的使用习惯是每两周进后台手动跑一次全量检测然后导出备份看一眼大小。这套系统动了太多次之后数据库迁移已经成了例行工作。从那以后我每次做配置变更前都强制走一遍备份流程导出SQLite数据库文件、备份config表、导出M3U列表三个文件打成一个包存档。这东西就像一个保险平时用不上真到数据库被写坏的时候不后悔药可吃。希望帮到你。本文还有配套的精品资源点击获取