StreamHall - 搭建你自己的直播间,从推流到观众统计一站式搞定

起因

之前逛到一个叫 AniLive 放映室 的 nsy 直播转播站,界面设计及功能实现我很喜欢 - 干净的卡片布局、暗色主题、多直播源管理,感觉刚好是想要的形态。可惜 ta 没有开源,也没有找到类似的替代品。

既然没有现成的,那就自己造一个。StreamHall 的界面风格参考了 AniLive 放映室的简洁直播列表形态,但前后端均为独立实现,不共享任何原始代码,并在此基础上扩展了观看统计、Telegram 推送、推流配置辅助等符合我个人使用的功能。

这也是我第一个完全通过 Vibe Coding 完成的项目。

功能概览

StreamHall 是一个自托管的直播站点系统,核心目标是自己搭、自己管、数据全掌握

首页 - 公开直播列表
管理后台 - 数据看板

演示站点:StarLive

主要功能:

  • 公开直播列表 - 直播 / 存档双 Tab,支持密码保护,站点标题、简介、导航链接、页脚内容均可在后台自定义,内置中英双语界面
  • 播放器 - 基于 ArtPlayer,支持 HLS、FLV、MPEG-DASH;可配置 AES-128 密钥覆盖、DASH ClearKey,以及 Widevine / FairPlay DRM 授权播放,播放器会根据浏览器环境自动选择合适的 DRM 方案
  • 管理后台 - 直播的增删改查、启用/禁用、拖拽排序,多播放源管理;中英双语后台
  • 观看统计 - 实时在线人数、今日/历史观看量、独立访客、平均时长、设备/浏览器/OS/地理分布看板,支持导出 CSV。该功能会记录访问时间、设备类型、浏览器、操作系统、IP / 地理信息等数据,公开部署时建议在站点说明或隐私政策中明确告知
  • Telegram 推送 - 可按直播单独配置,开播/关播自动发通知,消息模板支持自定义变量;多视角直播下每个视角上线都会独立推送,短时间内一起开播的视角会合并成一条消息,RTMP 短暂断线重连也不会误触发关播 / 开播通知
  • 本地文件推流 / VOD - 内置文件浏览器,可直接选择本地视频文件或整个文件夹通过 RTMP 推流;也可将视频文件直接发布为存档直播源,带签名 URL 和 HTTP Range 支持(可拖拽进度条)
  • RTMP 推流辅助 - 配合 SRS 容器,支持隐藏公开 HLS 路由(/h/<slug>/...),公开地址无法反推出真实推流码
  • HLS 反代 - 带签名验证的代理路由,解决外链 HLS 的跨域问题,提供完整代理 / 仅 Manifest / 直连等多种模式;针对依赖签名 Cookie 鉴权的 CDN(如 CloudFront)还可配置上游 Cookie,由服务端在转发时附带,Cookie 仅存于服务端、不会暴露在播放地址中。该功能只负责 manifest / segment 转发,不绕过 DRM、不提取密钥,也不会替代合法的 License Server 授权
  • DRM 辅助配置 - 后台可自动识别部分 DASH Widevine / HLS FairPlay manifest 中的 License URL、PSSH、Certificate URL 等信息,减少手动抓包配置成本
  • API 密钥 - 可在后台生成 Bearer Token,程序化访问所有管理和统计接口

技术栈

  • 后端:单文件 Python(server.py),无任何 Web 框架,基于标准库的 ThreadingHTTPServer。每个请求独立线程,每个线程独立 PostgreSQL 连接。
  • 数据库:PostgreSQL 16,唯一的外部运行时依赖是 psycopg3(连接驱动)。
  • 前端:纯原生 HTML + CSS + JS,不依赖任何前端框架,三个页面(首页、播放器、管理后台)各自独立。
  • 部署:Docker Compose,四个容器:应用本体、PostgreSQL、SRS(RTMP)、Nginx(HLS 代理)。

部署方式

只需 Docker Compose,大约两分钟可以跑起来。

第一步:下载配置文件

curl -LO https://git.stdm.moe/Stardream/StreamHall/raw/branch/main/docker-compose.yml
curl -LO https://git.stdm.moe/Stardream/StreamHall/raw/branch/main/nginx-hls.conf

第二步:修改必填项

打开 docker-compose.yml,把 SECRET_KEYPOSTGRES_PASSWORD 改成强随机值,这两个字段直接关系到会话安全和数据库访问权限,不能留默认值。

第三步:启动

docker compose up -d

第四步:获取初始密码

docker logs streamhall

在日志里找到这一行:

StreamHall initial admin password: <random-password>

初始密码只打印一次,不会以明文形式落库。启动后访问 http://HOST:8085/admin 登录,进后台后在网站设置 → 安全设置里改掉密码。

各服务地址

地址 说明
http://HOST:8085/ 公开直播列表
http://HOST:8085/admin 管理后台
http://HOST:18088/ SRS HTTP 播放
http://HOST:8889/ Nginx HLS 代理

日后更新

docker compose pull && docker compose up -d

几个值得说的设计细节

隐藏 HLS 路由

如果你用 RTMP 推流(比如 OBS),推流码是私密的,但如果直接把 SRS 的 HLS 地址暴露给观众,地址里会直接包含推流码(/live/<stream-key>.m3u8),任何人都能猜到推流码。

StreamHall 的做法是:在后台为每个推流码生成一个 HMAC 签名的 slug,公开播放地址变成 /h/<slug>/index.m3u8,由 Nginx 将请求反代到 StreamHall,StreamHall 验证签名后再转发给 SRS。观众拿到的地址无法反推出真实推流码。

这里也顺便说一下“隐藏链接”和“带宽压力”的取舍:如果外链 HLS 走 StreamHall 的完整代理,那么观众请求 manifest、分片、key 等内容都会经过自己的服务器,源地址隐藏效果更好,但本机也要承担观看流量。相反,如果让播放器直连源站,观众流量基本不经过自己的机器,但浏览器 Network 里最终还是能看到源站媒体地址。

所以 StreamHall 把 HLS 代理定位为“兼容性工具”:主要用来解决 CORS、HLS key、跨域播放失败等问题,而不是默认防盗链方案。目前每个播放源都可以单独选择代理模式:

  • 自动 / 完整代理 - manifest、分片、key 全部经过 StreamHall,源地址隐藏效果最好,但本机要承担观看流量;
  • 仅 Manifest - 只代理入口 m3u8,分片改写为直连源站地址,在“隐藏入口链接”和“不吃本机带宽”之间折中;
  • 直连 - 完全不经过代理,浏览器直接访问源站。

对于依赖签名 Cookie 鉴权的源(如 SPWN 用的 CloudFront),完整代理模式下还可以配置「上游 Cookie」,由服务端在转发 manifest 和分片时附带鉴权 Cookie。Cookie 只存在服务端,并以不可逆的签名 token 形式嵌入代理地址,观众无法从播放链接里还原出原始 Cookie。为了减少代理转发的延迟,对外链的上游请求还做了 HTTP 连接复用,避免每个分片都重新握手 TLS。

DRM 播放支持

v1.2.0 开始,StreamHall 支持为同一个播放源配置多组 DRM 授权信息。目前主要支持 Widevine 和 FairPlay:桌面 Chrome / Edge / Firefox 以及 Android 外部浏览器优先使用 Widevine,Safari / iOS 则优先使用 FairPlay,同一个“视角”也可以分别配置 Widevine 用的 MPD 链接和 FairPlay 用的 HLS 链接,让 Windows / Android 和 iOS / Safari 各走最合适的播放链路。

后台也加入了 DRM 自动识别。填入 DASH MPD 或 HLS manifest 后,StreamHall 会尽量从 manifest 中提取 Widevine License URL、PSSH,或 FairPlay Certificate URL / License URL。不是所有平台都会把授权信息完整写在 manifest 里,所以自动识别不能保证 100% 命中,但对 Brightcove 这类常见链路已经能省掉不少手动查找步骤。

需要强调的是,StreamHall 只负责把播放地址、License URL、Certificate URL 等合法授权信息交给浏览器的 EME 播放链路,不绕过 DRM,不提取密钥,也不解密内容。

实际接入 FairPlay 时还有个小坑:桌面端的 Widevine 请求比较容易在浏览器开发者工具里看到,但手机 Safari 走 FairPlay,必须找到对应的 FairPlay Certificate URL 和 License URL。我最后的做法是在手机端安装一个能提供类似 Safari DevTools 能力的调试 App,然后在已经授权、能够正常播放的页面里执行下面这段代码,把相关资源请求筛出来:

performance.getEntriesByType('resource')
.map(x => x.name)
.filter((url, index, list) => list.indexOf(url) === index)
.map(url => {
if (
/license\.live\.brightcove\.com\/cert\/fp\?/i.test(url) ||
/manifest\.prod\.boltdns\.net\/license\/v1\/fairplay_app_cert\//i.test(url)
) {
return { type: 'FairPlay Certificate URL', url };
}
if (
/license\.live\.brightcove\.com\/lic\/fp\?/i.test(url) ||
/manifest\.prod\.boltdns\.net\/license\/v1\/fairplay\//i.test(url)
) {
return { type: 'FairPlay License URL', url };
}
if (/manifest\.prod\.boltdns\.net\/license\/v1\/cenc\/widevine\//i.test(url)) {
return { type: 'Widevine License URL', url };
}
if (/edge\.api\.brightcove\.com\/playback\/v1\/accounts\/.+\/videos\//i.test(url)) {
return { type: 'Brightcove Playback API', url };
}
if (
/fastly\.live\.brightcove\.com\/.+\.m3u8/i.test(url) ||
/manifest\.prod\.boltdns\.net\/manifest\/v1\/hls\/.+\.m3u8/i.test(url)
) {
return { type: 'HLS Manifest', url };
}
if (/manifest\.prod\.boltdns\.net\/manifest\/v1\/dash\/.+\.mpd/i.test(url)) {
return { type: 'DASH Manifest', url };
}
return null;
})
.filter(Boolean)

对于 Brightcove 这类播放链路,筛出来的资源里通常可以看到类似 /cert/fpfairplay_app_cert/lic/fp/license/v1/fairplay/ 的请求,分别对应 FairPlay Certificate URL 和 FairPlay License URL。把这两个地址填进 StreamHall 同一个播放源的 FairPlay 配置里,iPhone / Safari 就能走 FairPlay 播放链路。

这些授权地址里的 token / cookies 往往有有效期,过期后需要从播放页重新获取。

对相关平台的实际验证

  • ニコニコ生放送

    Live

    Archive

  • Stagecrowd

    Live

    Archive

  • Streaming+

    Live

    Archive

  • SPWN

    Live

    Archive

  • ZAIKO

    Live

    Archive

  • PIA LIVE STREAM

    Live

    Archive

  • Z-aN

    Live

    Archive

  • ASOBI STAGE

    Live

    Archive

多语言支持

前端所有用户可见的文字都走 i18n 翻译表,包括服务端返回的错误信息 - 后端只返回错误码字符串(如 stream_not_found),前端根据当前语言翻译成对应文字,不会出现中文界面里突然蹦出一条英文报错的情况。

开源地址

项目代码托管在 GayHub 和 自建的 Gitea 上,基于 AGPL-3.0 协议开源,允许自由使用、修改和自部署,但若将修改版对外分发或作为托管服务提供,须以相同协议公开源码。