一、这套东西长什么样

打开网页或手机 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 立刻拦截,不转发了,开始自己干活

拦截之后它做四件事:

  1. 调 Emby 的 API 问一句:「这个 item 的文件路径是什么?」Emby 老实回答:/media/quarktv/music/song.flac
  2. 拿这个路径去比对配置里的前缀。命中了 → 判定这是个网盘文件,值得走直链
  3. 把这个路径翻译成 OpenList 里的虚拟路径,比如变成 /ali/music/song.flac
  4. 拿着翻译好的路径去问 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 主机,这才叫服务器。