一、这套东西长什么样
打开网页或手机 App,是熟悉的海报墙,点进去有简介、有封面、有分季分集,播放进度多设备同步,随时接着上次的地方看——跟你用任何一个正经流媒体平台的体验没什么区别。
区别在于片子一部都不存在我的硬盘里。所有影音文件都躺在网盘上,我的服务器只存了一堆索引和封面图,硬盘占用可以忽略不计。
实现方式是三个开源软件串起来:
| 软件 | 负责什么 |
|---|---|
| Emby | 门面。刮削、海报墙、播放进度、多端同步,用户看到的一切 |
| OpenList(AList 的分支) | 把网盘转成一个标准的 WebDAV 接口 |
| rclone | 把这个 WebDAV 挂载成服务器上的一个「本地目录」,好让 Emby 能读 |
网盘 → OpenList(AList) → rclone(FUSE 挂载) → Emby → 客户端
为什么要 rclone 这一层?因为 Emby 自己不能直接挂 WebDAV,它只认本地文件系统路径。中间这层不是多此一举,是硬性要求。
这套东西搭起来不算难,网上教程一堆。难的是最后一块拼图:默认情况下,播放时所有流量都会从你的服务器过一遍,小水管直接被撑爆。这篇文章讲的就是怎么解决它——让客户端绕开服务器,直连网盘。
二、先说说我为什么要折腾这个
最早我的流媒体是跑在自己那台 Windows 主机上的——白天当自用机打游戏写代码,顺手挂个 Emby 当服务器。
用了一段时间,问题全暴露了:
- 它得 24 小时开着。 一台配了独显的桌面机常年不关,电费肉眼可见地往上走,而绝大多数时间它只是在待机。
- Windows 不适合当服务器。 半夜自动更新重启、偶尔来一次蓝屏,人在外面想听首歌,发现服务已经躺了六个小时。
- 想干点别的就得停服。 关个机、重启一次,家里人正在看的剧就断了。
- 硬盘不够就得加。 加盘要开机箱,加完还得考虑备份。
后来换成了现在这套:存储交给网盘,索引和门面交给一台便宜的小服务器。
香在哪儿呢:
- 自用机想关就关,想重装就重装,跟服务完全解耦
- 存储空间基本不用操心,也不用自己做冗余备份
- 一台低配 VPS 或者小主机常年跑着,功耗几瓦
- Emby 该有的海报墙、刮削、播放进度同步、多端同步,一样不少
按上面那套架构搭完,一切正常,能刮削能播放,我以为大功告成了。
直到我看了一眼服务器的带宽监控。
三、致命问题:你的服务器在给客户端当搬运工
播一首几十 MB 的 FLAC,服务器的下行和上行同时在跑,而且跑的量跟文件大小几乎一致。
也就是说,数据流是这样的:
网盘 ──下行──> 我的服务器 ──上行──> 我的手机
服务器完整地下载了一遍,又完整地上传了一遍。
这对听歌来说勉强能忍,对看片来说是灾难。 一个 4K 视频动辄几十 Mbps 码率,一台小水管 VPS 直接原地爆炸。而且这还是双向消耗,很多云服务商的上行带宽是按量计费的。
我们真正想要的显然是这样:
网盘 ─────────直连─────────> 我的手机
(服务器只负责告诉手机「去哪儿拿」)
这就是所谓的 302 直连:服务器不传数据,只回一个 HTTP 302 重定向,告诉客户端「真正的文件在这个 URL,你自己去拿」。
四、为什么这套架构天生做不到直连
这一节是全文最关键的部分。想明白了,后面所有配置和坑都会变得很自然。
症结在于 FUSE 撒了个谎
rclone 的挂载用的是 FUSE,它的本质是把一个网络资源伪装成本地磁盘上的文件。伪装得太好了,好到 Emby 完全被骗过去了。
在 Emby 的世界观里,/media/quarktv/music/song.flac 就是一个躺在本机硬盘上的普通文件。
于是当客户端说「我要播这首歌」的时候,Emby 的反应完全合乎逻辑:
这是本地文件啊,那我
open()它,读出字节流,通过 HTTP 发给客户端就行了。
它压根不知道这个文件背后是网盘,也就永远不会想到要回一个 302。 你不能怪它,因为对一个真正的本地文件来说,回 302 是没有意义的——总不能让客户端自己来读你的硬盘。
那 OpenList 的「302 重定向」策略呢?
很多人(包括当时的我)会想:OpenList 后台不是有个 WebDAV 策略可以选 302 吗,把它打开不就行了?
不行,因为这个 302 是说给 rclone 听的,客户端根本听不见。
看清楚链路上谁在跟谁对话:
| 谁 → 谁 | 说了什么 |
|---|---|
| OpenList → rclone | 「文件在网盘那个 URL,你自己去拿」(302) |
| rclone | 「好的」,然后自己去网盘拿完,伪装成本地文件 |
| Emby → 客户端 | 读本地文件,把字节流发过去(200/206) |
OpenList 的 302 只让 rclone 少绕了一圈,流量该过服务器还是要过服务器。客户端在链路的最末端,它面对的只有 Emby,而 Emby 手里只有「本地文件」。
结论
只要播放请求是由 Emby 直接响应给客户端的,就一定走服务器带宽,无一例外。
想直连,就必须有人在 Emby 和客户端之间插一脚,在流量真正开始传输之前把这个请求截胡掉。
五、emby2Alist 是怎么解决的
emby2Alist 干的正是这件事:在 Emby 前面架一层 Nginx(配合 njs 模块),做一个会思考的反向代理。
以后你不再访问 Emby 的 8096,而是访问 Nginx 的 8091。
- 浏览海报墙、搜索、看详情 → Nginx 老老实实转发给 8096,跟普通反代没区别
- 一旦点击播放 → Nginx 立刻拦截,不转发了,开始自己干活
拦截之后它做四件事:
- 调 Emby 的 API 问一句:「这个 item 的文件路径是什么?」Emby 老实回答:
/media/quarktv/music/song.flac - 拿这个路径去比对配置里的前缀。命中了 → 判定这是个网盘文件,值得走直链
- 把这个路径翻译成 OpenList 里的虚拟路径,比如变成
/ali/music/song.flac - 拿着翻译好的路径去问 OpenList 要真实直链,然后把这个直链以 302 的形式甩给客户端
客户端收到 302,转头直连网盘满速拉取。服务器带宽占用归零。
一个反直觉但很重要的点
我配的时候卡了一下:Nginx 容器里根本没有 /media 这个目录,那它怎么读文件?
它从头到尾没读过任何一个媒体文件。
上面四步里没有任何一步涉及文件 I/O,全是字符串比对和字符串替换。Nginx 只是个「处理字符串的中间人」,它需要知道的只有「路径长什么样」,不需要真的能访问到那个路径。
想通这一点,你就理解了这套方案为什么优雅:容器之间彻底解耦,Nginx 不需要挂载任何媒体目录。
也正因为一切都是字符串匹配,所有的坑都出在字符串对不齐上。
六、怎么配
前提:你已经有一套能正常播放的 OpenList + rclone + Emby,只是走的服务器流量。
1. 拉配置文件
mkdir -p ~/emby2Alist
cd ~/emby2Alist
# 版本号请到 Releases 页面确认最新的
wget https://github.com/bpking1/embyExternalUrl/releases/download/v0.0.1/emby2Alist.tar.gz
tar -xzvf ./emby2Alist.tar.gz -C ~/emby2Alist
2. docker-compose.yml
services: nginx-emby: image: nginx:1.29
container_name: nginx-emby
restart: always
ports: - 8091:80
volumes: - ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/log:/var/log/nginx
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./embyCache:/var/cache/nginx/emby
3. 改三个文件(新版配置是拆分的)
早期教程说「只改 constant.js」,但新版把配置按功能拆到了 config/ 目录下,constant.js 顶部全是 import。你会在里面找不到填 OpenList 地址的地方,别怀疑人生。
① conf.d/constant.js
const embyHost = "http://172.17.0.1:8096";const embyApiKey = "在 Emby 后台生成的 API Key";const mediaMountPath = ["/media"]; // 见下面「坑一」,这行最容易错
② conf.d/config/constant-mount.js
const alistAddr = "http://172.17.0.1:5244";const alistToken = "OpenList 后台 → 设置 → 其他 → 令牌";
**③ `conf.d/config/constant-pro.js`**
这个文件几百行,第一次打开像看天书,全是各种高级路由规则。**别慌,99% 的内容都不用碰:**
|配置项|干嘛的|要改吗|
|---|---|---|
|`redirectConfig`|直链总开关,分别控制视频/音频/直播/下载|默认全开,不用改|
|`routeCacheConfig`|直链结果缓存,防止你狂点导致频繁请求 OpenList|不用改|
|`routeRule`|按码率/分辨率/设备/用户做精细分流|**千万别碰**,保持注释|
|`mediaPathMapping`|**路径翻译,唯一必改项**|**必改**|
|`alistRawUrlMapping`|对 OpenList 返回的直链再替换一次(套 CDN 才用)|不用改|
```javascript
const mediaPathMapping = [
// [替换方式, 路径类型, 旧路径, OpenList 里的虚拟路径]
[0, 0, "/quarktv", "/ali"],
(第一个 `0` = 做一次字符串 replace;第二个 `0` = 只处理本地物理路径。)
### 4. Emby 里必须做的设置
访问 `http://你的IP:8091` 进 Emby(别再用 8096 了),然后:
- 播放设置里把「互联网质量」「本地网络质量」全部拉到**最高 / 原始**
- 进用户权限,**关掉该用户的转码权限**
为什么这步不是可选的,见「坑三」。
---
## 七、四个真正会卡住你的坑
### 坑一:Emby 报出来的路径,不是你在服务器上看到的路径 ⭐
**这是最大的坑,我在这上面耗掉了大半天。**
我的 rclone 挂载点在宿主机上是 `/mnt/webdav/ali`,所以我理所当然地把 `mediaMountPath` 填成了 `["/mnt"]`。
结果一直返回 206(也就是没走直链)。翻日志才看到真相:
```text
js: mount emby file path: /media/quarktv/music/xxx.flac
js: hit proxy, localFile not mountPath first: ["/mnt"]
js: use original link
因为 Emby 自己也跑在 Docker 里。 宿主机的 /mnt/webdav/ali 被映射进 Emby 容器时,落点是 /media/quarktv——Emby 数据库里存的、API 返回的,全都是这个容器内部的路径。
脚本拿 /media/quarktv/... 去和 /mnt 比对,前缀对不上,判定「这不是网盘文件」,于是老老实实退化成反代。
一句话记住:
mediaMountPath要填的是 Emby 容器内部看到的路径前缀,不是你ls时看到的宿主机路径。
不确定的时候别猜,直接问 Emby:
curl "http://你的EmbyIP:8096/Items?Ids=项目ID&Fields=Path&api_key=你的KEY"
返回的 Path 字段是什么,你就照着写什么。这一条能省下一整个下午。
坑二:mediaPathMapping 的源路径,前缀已经被砍掉了
官方注释里藏着一句极不起眼但要命的话:
路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍……注意 /mnt 会先被移除掉了
意思是:走到 mediaPathMapping 这一步时,mediaMountPath 那段前缀已经不在字符串里了。
所以我的映射规则源路径应该写 /quarktv,而不是 /media/quarktv。写全了反而匹配不上。
完整推演一遍就很清楚了:
Emby API 返回: /media/quarktv/music/song.flac
前缀命中 /media → 判定为网盘文件,走直链流程
砍掉前缀: /quarktv/music/song.flac
应用映射规则: /ali/music/song.flac
拿这个去问 OpenList 要直链 → 302 Found ✅
坑三:只要转码,直连必然失效
转码和 302 直连在原理上就是互斥的。
转码意味着 Emby 必须用 FFmpeg 亲自读取原始文件,重新编码,再把新的流发出去。这个过程中根本不存在「一个客户端可以直接下载的完整文件」,自然也就没有直链可给。脚本一旦发现在转码,就会自动放弃 302。
所以只要客户端触发了转码——不管是浏览器不支持这个编码,还是 Emby 觉得「网络质量不佳」自作主张降码率——你的服务器立刻打回原形,继续搬运流量。
这就是为什么前面那两步 Emby 设置是必做项,而不是优化项。
判断方法:播放的同时用管理员账号打开 Emby 仪表盘,看那张播放卡片写的是「直接播放」还是「转码」。写着转码,就不用往下查了,先解决转码。
坑四:docker logs 里什么都看不到
排错时我发现 docker logs -f nginx-emby 只有启动信息,一条业务日志都没有。
因为 compose 里把 ./nginx/log 挂到了容器的 /var/log/nginx,把官方镜像里原本指向 stdout 的软链接顶掉了。 日志全写进宿主机的实体文件了:
tail -f ~/emby2Alist/nginx/log/error.log
njs 脚本的完整决策链路都以 js: 开头打在这里——拿到的文件路径、有没有命中前缀、最终判定是 redirect 还是 proxy,全都一清二楚。排这套东西的时候,这个文件是唯一有用的线索,一定要先找到它。
八、怎么确认真的成功了
浏览器 F12 → Network,播放一首歌,找 /Audio/xxx/stream 或 /Videos/xxx/stream 这类请求:
- 302 → 成了。紧接着会出现一条新请求,直接连向网盘的域名
- 200 / 206 → 没成,Nginx 退化成了普通反代,服务器还在搬砖
同时盯一眼服务器带宽监控。如果视频秒开、拖进度条丝滑,而带宽曲线几乎是平的——那就是真的通了。
九、这套方案的代价
天下没有免费的午餐,直连是有取舍的:
- 不能转码。 客户端不支持的编码只能回落到走服务器。所以尽量保证片源编码是客户端原生支持的。
- Web 端的内封字幕基本没法用。 Emby 提取内封字幕需要 FFmpeg 读取整个视频文件,等于把片子完整下载一遍,还费 CPU。Web 端建议一律用外挂字幕。
- 依然依赖网盘的稳定性。 网盘限速、Token 过期、接口变动,你都得跟着修。这是用网盘当硬盘的固有成本。
但对我来说完全值得:一台几瓦的小机器,跑着一个随时可用、想加片就加片的媒体库,自用机爱怎么折腾怎么折腾。
比起以前那台半夜蓝屏的 Windows 主机,这才叫服务器。