Loading...
时易うさぎのBlog
开源小项目闲聊

为了在VRC和砂糖一起看视频,我vibe了一个Jellyfin中转服务

事情的起因非常简单:跟砂糖们去动漫屋一起看番,但是动漫屋的资源都没办法正常加载,或者有的人能加载出来有的人加载不出来。直接去b站找资源用直连播放,也是类似的情况。但我想跟砂糖一起看的这个番,在我NAS上也有。

于是我就想把 NAS 上的视频放到 VRChat 里和砂糖一起看。

但是现有的视频文件转云链的服务,要么有各种各样的限制,要么体验可能还不如动漫屋。

类似MediaMTX之类的项目使用起来又不够无脑,又不能完美贴合我的需求,可能后续还需要做一些调整。

所以干脆就手搓一个吧,帮帮我,GPT先生!

需求也不复杂,能播放,能暂停,能拖进度条。复制一个链接进房间播放器,就跟平常直接用b站解析的直链一样。

然后我就从单纯传一个链接,一路折腾出了一个带共享缓存、NAS 转码、字幕烧录、预载、动态限速和管理页面的小项目。

现在它已经开源了,名字叫 一起看 · Jellyfin VRC Relay

项目地址:Tokiichika/jellyfin-vrc-relay

这篇先聊聊它是怎么来的,以及为什么最后会长出这么多功能。

能放是能放,但流量不太对

我的 NAS 上装着 Jellyfin,最直接的办法就是拿到串流链接,塞进 VRChat 世界里的播放器,完全不需要任何其他的操作。

但这里有个问题:房间里看起来是一块大屏幕,实际拉视频的却是每个人自己的客户端。

也就是说,五个人一起看,通常就是五个人分别向我的 NAS 请求视频。我这边又用了内网穿透,重复传输既吃上传带宽,也吃穿透流量,就我家宽带50M上行的小水管,估计也扛不住俩仨人(

假设视频平均码率是 8Mbps,五个人同时观看,NAS 的持续上传需求就至少要 40Mbps。实际还会受播放器缓冲、音频和其他开销影响,这里只是随便算个大概。

刚好我还有一台标称不限流量、200Mbps 带宽的云服务器,于是就有了一个想法:

能不能让云服务器去 NAS 拿视频,再由它发给客户端?

大概是这样:

不过,光加一层普通反向代理还不够。如果每个观众的请求都原样转回 NAS,那只是绕了一圈,重复上传的问题照样存在。

真正要做的是共享缓存:已经拿到的数据留在云端,后面的人直接复用;几个人同时请求同一块还没缓存的数据,也尽量合并成一次回源。

当然,这不代表一部视频永远只从 NAS 下载一次。缓存被淘汰、转码参数不同,或者请求失败需要重试时,还是可能再次下载。云服务器也仍然要承担所有观众的出口流量。但不管怎么说,带宽和流量的开销都肯定要远远小于直接使用Jellyfin的串流链接。

我想要点播,不是把它变成直播

另一个从一开始就确定的需求是:我要能暂停和拖动进度。

所以这套东西围绕点播来做。原文件走字节范围请求,也就是播放器可以指定“我要文件的这一段”;需要 Jellyfin 转码时,则使用 HLS 点播分片。

可以把分片理解成把视频拆成一小段一小段,播放器播到哪里,就取对应的部分,云服务器也按需要缓存。

这也对我云服务器的配置比较友好,我也不想为了看一集视频,就先在云端存下整部原片。视频缓存默认上限设成了 9GB,后来又做成可配置的。

这里的9GB指单纯的视频缓存,不是 Docker 镜像、日志和整个系统加起来都只占 9GB。

顺便说一下,房间里谁来控制播放、如何让大家同步进度,还是世界播放器负责。这个项目负责提供视频链接和数据,不接管房间的同步逻辑。

VLC 播得好好的,怎么进 VRC 就卡了?

最开始让我比较迷惑的是,同一个视频,在 Jellyfin 网页里播放没什么问题,用 VLC 打开原始链接也很流畅,但进 VRChat 就一直卡。

当时那份素材的视频编码是 HEVC,也就是 H.265,音轨是 FLAC。

后来我换了一份 H.264 视频、AAC 音频的素材,VRC 里就不卡了。

这组对比当然不能直接证明“所有问题都是 HEVC 导致的”,毕竟视频和音频都换了。但至少给了我一个很有用的方向:不能因为 VLC 能放,就默认房间播放器也能舒服地处理同样的编码和封装。

Jellyfin 网页能正常播放,也不一定代表它正在直放原文件,它可能已经替浏览器做了转码。

于是项目加上了让 NAS 输出 H.264 / AAC 的功能,默认参数是:

  • 视频 H.264,最高 1080p,目标码率 8Mbps。

  • 音频 AAC,立体声,192kbps。

  • 视频和音频是否转码、分辨率和码率,都可以自己调整。

这个默认值主要是为了兼容和方便起步,不是什么适用于所有片源的“最佳画质参数”。本来就适合播放器的视频,可以保留兼容轨道,或者直接走原文件模式,省下一次重新编码。

至于转码放在哪里做,我没有让那台2核4G的云服务器硬扛。

我当时的 NAS 是 E5 平台,配了一张Tesla P4,Jellyfin 版本是 10.10.7。转码交给 NAS 上的 Jellyfin,云端负责缓存和分发,分工就比较清楚了。

后来也把 NAS 硬件加速的相关设置做进了管理页,方便以后换 Intel 核显或者其他显卡时调整。不过驱动、设备映射这些基础环境,还是得先在 NAS 上配置好。

画面顺了,字幕又没了

视频播放正常以后,我又发现:字幕呢?

明明 Jellyfin 里有外挂字幕,网页端也能显示,怎么复制链接过去就没有了?

原因其实很好理解。外挂字幕是另一份文件,内封字幕也是独立的轨道。Jellyfin 客户端知道怎么把它们拿出来显示,但房间播放器只拿到视频流,不一定会顺带获取或正确显示字幕。

既然已经可以转码,那就干脆加个字幕烧录。

这个字幕烧录以及刚刚的重新编码,都是Jellyfin自带的功能,复制的Jellyfin串流链接里面带apikey,而这个服务使用这个串流链接回源的时候,修改串流链接所带的参数就可以指定转码、字幕烧录了。

管理页先读取 Jellyfin 识别到的字幕列表,有字幕时默认选第一条,再按需要改成正确的语言或版本。选择烧录后,NAS 会把字幕直接画进视频画面,再重新编码输出。

这样一来,观众看到的画面里已经有字幕,不需要额外折腾播放器的字幕支持。

代价也很直观:烧进去以后,这个链接里的字幕就不能随手关掉或切换语言了。要换字幕,需要重新生成链接。它还会增加 NAS 的处理负担,字体缺失、ASS 样式和复杂特效的效果,也需要结合 NAS 上的实际环境检查。

对我这种提前选好一条字幕、大家一起看的场景来说,这个取舍还是比较合适的。

首次加载太慢?那就提前等

还有一个很明显的问题是首次加载。

我遇到过播放器长时间显示载入中,偶尔还提示格式错误,等了好一会儿才开始播放的情况。但重新进房间,用同一个链接再放,因为服务器上已经有缓存,起播就快了很多。

这并不能解释所有加载报错,不过至少说明,冷启动时的等待值得单独处理。

第一次播放可能要等 Jellyfin 启动转码、生成开头分片,然后云服务器再把这些分片拿到手。大家坐下来以后再一起等这一轮,体验确实不太好。

于是我加了一个手动预载按钮。

默认预载视频开头的 5%,按时长计算,并取完整分片。比如 25 分钟的视频,目标大约是前 75 秒。不过后来我发现8%可能更好一些,具体预载多少还是要看世界播放器的类型以及看的视频的时长。

喊砂糖们来之前先点一下预载,把开头的转码和下载提前做掉,等砂糖们进来再播放,就更从容一些了。

这个功能没有让转码凭空变快,只是把一部分等待挪到了观看之前。拖到没缓存的位置,依然可能要等。我当时实测拖进度条大概有四五秒延迟,Jellyfin网页转码播放也差不多,所以可以认为这个是纯纯转码带来的延迟,跟回源还要带宽没关系。

8Mbps 的视频,怎么把出口跑到 180Mbps 了?

做了流量面板以后,我又看到了一个挺有意思的现象:房间里只有我一个人,向观众发送的瞬时速度,有时候却能冲到 180Mbps。

一开始很容易把视频码率和下载速度混在一起。但播放器不一定每秒只下载一秒的视频,它会提前缓冲,也可能在拖动之后重新请求数据。

比如一段 10 秒、平均 8Mbps 的视频,大约有 80Mb 数据。如果播放器一秒就把它下载完,那这一秒的传输速度就能到约 80Mbps。再加上实际码率波动,短时间的速度比视频码率高并不奇怪。

所以,不能只因为看到峰值高,就断定数据被重复发送了;但如果完全不限,多个播放器一起抢缓冲,确实可能把总出口吃满。

我最开始的想法是,把 200Mbps 的 80% 拿出来分配,再除以客户端数量。后来又加上单个出口 IP 的上限和最低接纳带宽。

按目前的默认设置:

  • 总预算是 160Mbps,也就是 200Mbps 的 80%。

  • 单个出口 IP 默认最多 32Mbps,避免一个人把缓冲拉满整个出口。

  • 动态分配时,每个已接纳的出口 IP 至少要有 10Mbps 的预算。

  • 如果再加一个新 IP 就不够分了,拒绝新增请求,优先保留已经在看的客户端。

按这个例子算,仅看总预算和最低门槛,最多是 16 个出口 IP(也就是说我的砂糖容量是16吗,咳咳)。但这只是接纳规则,不是“保证16人无卡顿”的性能承诺。源站速度、服务器线路和视频实际码率,仍然会影响播放。

这里特意说的是出口 IP,不是精确人数。同一个网络下的几个人可能共用一个公网 IP,所以页面上的活跃客户端数也只能作为估算来看。

B 站直链本来就能放,为什么也要支持中转?

再后来,我把 B 站视频也加进来了。

直链解析的实现则是参考这个油猴脚本:mmyo456/BiliAnalysis

这件事听起来好像有点多余:B 站解析后的直链,本来就可以直接丢给播放器,为什么还要经过我的服务器?

确实,如果大家直连都很流畅,直接放就好了,也省得占用自己的云端带宽。因为当时我有一个猜测:B站解析出来的直链会根据每个人所在地理位置的不同发生变化,CDN的节点不同。而这可能会导致一个人解析出来的直链丢到播放器里面,她自己能看,别人可能能看也可能直接加载失败。

而在自己电脑上解析出来的CDN地址,未必适合云服务器所在的网络,那么让服务器自己解析,应该是会更合适的。

这部分我没有把它写成确定结论。服务器解析不等于自动选出最快节点,具体还得看实际返回的地址和回源速度。中转本身又多了一跳,服务器线路不好时,完全可能比直连更慢。

这个功能更多的是把世界里所有人的观看体验尽可能拉平,当出现有的人能放,有的人放不了的问题时,作为一个备用手段。

目前B站视频播放这条流程主要处理受支持的单文件H.264/AAC MP4,负责缓存和分发,不是在云端再做一套万能转码,也不承诺所有视频都能解析。

功能越加越多,页面也得跟上

早期我更关心的是“能不能播”。等基本流程跑通,才发现很多小地方用起来很别扭。

比如主页连一次管理密钥,设置页还要再连一次;切个页面整个网页重载;复制链接没有成功提示;保存设置失败了也不够明显。

这些问题单看都不大,但真正坐下来用的时候,会不断打断操作。

所以后来把主页和设置页合并成了一套界面,共用登录状态,并在当前标签页会话中保留登录状态,减少刷新时重复输入密钥的麻烦。生成链接、复制、保存配置,都补了成功或失败提示,按钮也会区分连接状态。

面板里逐步加上了发送和回源速度、缓存占用、系统 CPU 和内存、活跃客户端估算,以及可以按需打开的调试日志。数据默认每三秒刷新,也能自己修改。

外观上则试了一点所谓液态玻璃风格,加上比较克制的背景光。最后用的是本地 CSS、SVG 和原生 JS,没有为了效果再引入一大套重型渲染库,也不依赖外部 CDN 加载前端资源。

毕竟它主要还是个管理工具,我希望它好看一点,但不想为了几个发光按钮把浏览器搞的能让风扇转起来。

整理一下,干脆开源

做到这里,我觉得这已经不只是我自己临时用一下的脚本了。遇到类似需求的人(虽然这个需求可能比较小众),可能也能省掉一部分重复折腾,所以就把它整理后开源了。

项目采用 MIT 许可证。开发过程中,我负责提出需求、在自己的 NAS 和 VRChat 环境里测试、反馈问题,再和 Codex 一起逐步调整实现。Codex 协助完成了代码、界面、排查、测试和文档工作。

回头看,很多功能都不是一开始就计划好的:

卡顿了,就检查编码;字幕没了,就加烧录;冷启动太慢,就做预载;看到出口峰值太高,就加限速;自己操作着别扭,就继续改交互。

基本就是“用起来,发现问题,再改”的过程。

开源前还专门整理了脱敏配置和部署流程。准备好 Linux、Python 3、Docker 和 Compose 后,解压完整部署包,在目录里执行:

sudo bash install.sh

脚本会引导填写公开 HTTPS 地址和 Jellyfin 主机名,生成管理密钥及配置,然后构建、启动服务。不需要再手动给配置文件改名,或者额外跑一次迁移脚本。

域名、证书、反向代理和防火墙这些,仍然需要按自己的环境配置。具体步骤和现阶段的验证范围都放在仓库文档里,部署前建议看一下。

到了现在,我差不多刚好用完了一个Plus套餐的Codex(GPT6-Astra-Low)的周额度(

最后说两句

这个项目比较适合这样一种情况:家里有NAS、并且有Jellyfin,能够通过公网访问,上传带宽或穿透流量比较宝贵,手里又有一台线路和带宽合适的服务器,有好几个砂糖要一起看视频(

当然如果你就算没NAS,在自己电脑上装个Jellyfin,并导入存在电脑上的番剧或其他视频也是一样的。

如果原来的链接已经播得很好,就不一定需要多加这一层。它解决的是我实际碰到的一组问题,不是所有视频播放场景都必须经过的步骤。

另外叠个甲,工具开源不等于内容也获得了授权。请只处理和分享自己有权提供的内容,不要用于非法用途或传播违法违规内容。管理密钥和“仅供个人使用”的提示只是访问管理手段,不能代替内容授权或部署时需要考虑的合规要求;相关说明也整理在了 README 中。

最开始我真的只是想和砂糖一起看个视频。现在至少,下次再遇到动漫屋里面资源放不了,或者有的人能看有的人不能看,不用只对着一个“载入中”猜半天了。

如果你也有类似需求,可以看看项目,有问题或者建议欢迎提 Issue,当然有没有时间改我就不好说了(逃。

仓库:一起看 · Jellyfin VRC Relay

最后再晒一下我和我的饱饱们(

当然还是那句话,你找砂糖我不推荐,我找砂糖你别管(

分享到

评论