本文最后更新于 8 个月前,文中所描述的信息可能已发生改变。
WARNING注意
2024-03-29 更新:该文章大部分内容已经过时,无更多参考价值,具体更加完善的 Emby 加速方案正在撰写中,敬请期待…
方案设想
目前使用的是Racknerd洛杉矶VPS作为Emby服务器,除了晚高峰,其余时间播放速度体验尚好,但就在晚高峰,观看体验骤降,速度一度跑不上100kb/s,使用体验堪称国内运营商超出限制的无限流量套餐。
中转
脑中蹦出的第一个方案则是全程挂梯子 脑中蹦出的第一个方案当然是中转,使用国内大带宽Nat或香港日本韩国的vps反代emby服务器。
中转服务器的选择
国内Nat先不说价格,除了广东上海,很难说其他地理位置的机子中转访问emby的体验和直连观看有什么区别。其次是域名解析到国内ipv4需要备案,端口也不是常规的80、443、8096。线路优秀的香港日本VPS价格昂贵,但我的标题中打上了“低价”字样,所以祭出第一个方案:Azure100订阅
Azure100 香港
之前的微软香港云还是可以直连的,但是经历了拔线风波之后统一绕路再入境。不过Emby 嘛又不是什么奇怪的东西,延迟什么50,100体验没什么区别,不过azure100的服务器月流量到底是15G还是115G就不好说了,网上吵了一年还是没有结果,不过网上冲浪的时候发现一个网友没注意流量跑了2000+rmb… 微软的东西还是小心点好,so 15G月流量,想跑emby,全剧终
CDN
TL & DR :Cloudflare CDN不如直连美西,微软CDN体验极佳但是流量费近1元1G,用国内CDN没有备案,尝试阿里云国际CDN体验如同Cloudflare(甚至还不如),全剧终…
其实在这方面还是有做过尝试的,比如cloudflare优选IP。在当时那个时间段短暂体验过一会儿Cloudflare台湾高雄机房,体验非常好,三网延迟低至50ms(比az香港都好)。不过Cloudflare嘛,线路好的机房注定会被淘汰的。不出一个月,台湾高雄机房的IP段全面被墙。
同时由于Cloudflare使用的知名度之高,国内外同样有一些线路好的VPS反向代理Cloudflare当地节点实现加速的行为。但由于常规的反向代理不会验证使用者,导致了有一些IP可以被随意使用,具体可以查看遨游者的博文:反代/中转cloudflare的安全隐患与隐患利用。网上有一些专门扫面这些IP的项目,但是白嫖使用别人的机器优化自己的使用体验总是违背道德的,同时使用的人数一但躲起来免不了落得和cloudflare一样的下场。
树莓派
这时目光来到了手上的树莓派上,其实用家宽公网ipv6+DDNS,再跑上一个clash用于加速到emby的连接应该体验也不错,30M的上行挤挤也够用,只不过纯ipv6的访问体验在某些地方可能不佳。
Emby播放直链
这是本篇的重头戏,利用nginx反代搭配alist,可以把emby的播放直链劫持导向网盘直链,视频流量完全不经过服务器,这时的播放体验和使用的网盘的速度挂钩。
当然缺点就是无法转码播放,因为视频资源完全不通过服务器。不过就服务器那点配置估计也跑不动转码吧(
前提条件就是一个国内体验不错的大容量网盘,谷歌云盘直接淘汰 除此之外就是阿里云和onedrive的选择。不过阿里云的容量只有3t,并且有和谐资源的嫌疑,相比之下微软E5订阅的5T容量看起来就非常不错,唯二的缺点可能就是微软api申请的风控策略,并且也不知道什么时候会凉。(不过播放也是用alist调用微软的api,间接给E5续命)
具体实施
香港中转
使用nginx反代cf节点非常简单,这里推荐BT面板的仿制品**mdserver-web** 不用担心BT面板的后门,也可以获得相似的体验。
具体做起来只有两个坑:
- 申请ssl证书疯狂报请关闭301重定向,即是从来没开启过
- 反代cf站点报错403
第一点可能是因为面板的逻辑问题,首先配置了反向代理就会出现这种情况,这时再关闭反向代理也会一直提示 请关闭301重定向,具体解决方法就是删了站点重建,先申请ssl证书再配置反向代理。
第二点需在反代配置中加上 proxy_ssl_server_name on;
CDN
这里主要是用了ip-scanner /cloudflare 项目,此项目收集了全网用于反代cf的机子,只需要稍加配置即可食用。
这里我让ChatGPT写了一个python脚本用于扫描此项目里有哪些ip可用,ChatGPT使用request库挨个请求每个ip的80,443端口,返回403就请求/cdn-cgi/trace
确定是用于中转的ip,可用就填进字典里;不过感觉这样效率很低,如果用上述博文《反代/中转cloudflare的安全隐患与隐患利用》中的方法应该会快一些
对于地区的选择,如果选择国内机子搭配的域名仍然需要备案。。并且只有广东的服务器体验会好一些,相比之下除了使用香港台湾新加坡的机子体验会很不错。
项目中的大部分ip会每日更新,不过我还是会喜欢几个月前的老ip,总给人一种“已经活了这么久的ip应该会活的更长一点”的错觉。
树莓派中转
同样是使用mdserver-web搭建中转,同样需要添加proxy_ssl_server_name on;
避免访问403,这里卡住的点是如何用将nginx反代的网站用socks再代理一下,具体文章参考Nginx 如何与 Socat 配合使用
1. 安装支持socks5的socat
sudo apt install -y autoconf git yodl curl make
# 拉取源码
git clone https://github.com/runsisi/socat.git
# 进入目录
cd socat
# 处理配置
autoconf
./configure --prefix=/usr
# 编译
make
# 安装
sudo make install
# 检查是否成安装
socat -h
2.用screen做守护进程
原文用的systemd,我感觉screen更加方便一些。假设本地socks5代理端口是1080,将代理完的服务暴露在1081端口:
screen -R socat
... #测试
socat TCP4-LISTEN:1081,reuseaddr,fork SOCKS5:127.0.0.1:cip.cc:80,socks5port=1080
# CTRL+A+D挂起
curl -v -H "Host: cip.cc" 127.0.0.1:1081 #测试是否代理成功
...
screen -r socat
socat TCP4-LISTEN:1081,reuseaddr,fork SOCKS5:127.0.0.1:Emby服务器IP:8096,socks5port=1080
# CTRL+A+D挂起
之后在nginx中直接反代127.0.0.1:1081即可
实际测试起来只要树莓派访问代理的速度不错,体验就很好,我的媒体库里的视频都不大,也没有上行触及30Mbps的上限
Emby直链
GitHub仓库: embyExternalUrl,本机已经装好alist,配置好网盘,部署好emby。按理说alist不部署在本机也是可以的,但是实际测试后请求alist总是超时
克隆仓库到本地后修改docker-compose文件(因为已经部署好了alist):
version: '3.5'
services:
service.nginx:
image: nginx:alpine
container_name: emby-nginx
ports:
- 8095:80
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/embyCache:/var/cache/nginx/emby
restart: always
接着修改emby.js文件:vim nginx/conf.d/emby.js
关于rclone的挂载目录指的是rclone挂载emby媒体库的路径,如我将网盘/movie
挂载在本地/mnt
目录下,则填/mnt,根目录下就填/即可。Emby的视频文件路径在/mnt/movie/xxx
,但是alist中我的网盘路径是/movie/xxx
,所以需要脚本把挂载目录去除。
//author: @bpking https://github.com/bpking1/embyExternalUrl
//查看日志: "docker logs -f -n 10 emby-nginx 2>&1 | grep js:"
async function redirect2Pan(r) {
//根据实际情况修改下面的设置
const embyHost = 'http://172.17.0.1:8096'; //这里默认emby/jellyfin的地址是宿主机,要注意iptables给容器放行端口
const embyMountPath = '/mnt'; // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里 就填写 /mnt
const alistToken = 'alsit-123456'; //alist token, 在alist后台查看
const alistAddr= 'http://172.17.0.1:5244'; //访问宿主机上5244端口的alist地址, 要注意iptables给容器放行端口
const embyApiKey = 'f839390f50a648fd92108bc11ca6730a'; //emby/jellyfin api key, 在emby/jellyfin后台设置
const alistPublicAddr = 'http://youralist.com:5244'; // alist公网地址, 用于需要alist server代理流量的情况, 按需填写
//fetch mount emby/jellyfin file path
const regex = /[A-Za-z0-9]+/g;
const itemId = r.uri.replace('emby', '').replace(/-/g, '').match(regex)[1];
const mediaSourceId = r.args.MediaSourceId ? r.args.MediaSourceId : r.args.mediaSourceId;
const Etag = r.args.Tag
let api_key = r.args['X-Emby-Token'] ? r.args['X-Emby-Token'] : r.args.api_key;
api_key = api_key ? api_key : embyApiKey;
const itemInfoUri = `${embyHost}/Items/${itemId}/PlaybackInfo?MediaSourceId=${mediaSourceId}&api_key=${api_key}`;
r.warn(`itemInfoUri: ${itemInfoUri}`);
const embyRes = await fetchEmbyFilePath(itemInfoUri, Etag);
if (embyRes.startsWith('error')) {
r.error(embyRes);
r.return(500, embyRes);
return;
}
r.warn(`mount emby file path: ${embyRes}`);
//fetch alist direct link
const alistFilePath = embyRes.replace(embyMountPath, '');
const alistFsGetApiPath = `${alistAddr}/api/fs/get`;
let alistRes = await fetchAlistPathApi(alistFsGetApiPath, alistFilePath, alistToken);
if (!alistRes.startsWith('error')) {
alistRes = alistRes.includes('http://172.17.0.1') ? alistRes.replace('http://172.17.0.1',alistPublicAddr) : alistRes;
r.warn(`redirect to: ${alistRes}`);
r.return(302, alistRes);
return;
}
if (alistRes.startsWith('error403')) {
r.error(alistRes);
r.return(403, alistRes);
return;
}
if (alistRes.startsWith('error500')) {
const filePath = alistFilePath.substring(alistFilePath.indexOf('/', 1));
const alistFsListApiPath = `${alistAddr}/api/fs/list`;
const foldersRes = await fetchAlistPathApi(alistFsListApiPath, '/', alistToken);
if (foldersRes.startsWith('error')) {
r.error(foldersRes);
r.return(500, foldersRes);
return;
}
const folders = foldersRes.split(',').sort();
for (let i = 0; i < folders.length; i++) {
r.warn(`try to fetch alist path from /${folders[i]}${filePath}`);
let driverRes = await fetchAlistPathApi(alistFsGetApiPath, `/${folders[i]}${filePath}`, alistToken);
if (!driverRes.startsWith('error')) {
driverRes = driverRes.includes('http://172.17.0.1') ? driverRes.replace('http://172.17.0.1',alistPublicAddr) : driverRes;
r.warn(`redirect to: ${driverRes}`);
r.return(302, driverRes);
return;
}
}
r.error(alistRes);
r.return(404, alistRes);
return;
}
r.error(alistRes);
r.return(500, alistRes);
return;
}
async function fetchAlistPathApi(alistApiPath, alistFilePath, alistToken) {
const alistRequestBody = {
"path": alistFilePath,
"password": ''
}
try {
const response = await ngx.fetch(alistApiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': alistToken
},
max_response_body_size: 65535,
body: JSON.stringify(alistRequestBody)
})
if (response.ok) {
const result = await response.json();
if (result === null || result === undefined) {
return `error: alist_path_api response is null`;
}
if (result.message == 'success') {
if (result.data.raw_url) {
return result.data.raw_url;
}
return result.data.content.map(item => item.name).join(',');
}
if (result.code == 403) {
return `error403: alist_path_api ${result.message}`;
}
return `error500: alist_path_api ${result.code} ${result.message}`;
}
else {
return `error: alist_path_api ${response.status} ${response.statusText}`;
}
} catch (error) {
return (`error: alist_path_api fetchAlistFiled ${error}`);
}
}
async function fetchEmbyFilePath(itemInfoUri, Etag) {
try {
const res = await ngx.fetch(itemInfoUri, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Content-Length': 0,
},
max_response_body_size: 65535,
});
if (res.ok) {
const result = await res.json();
if (result === null || result === undefined) {
return `error: emby_api itemInfoUri response is null`;
}
if (Etag) {
const mediaSource = result.MediaSources.find(m => m.ETag == Etag);
if (mediaSource && mediaSource.Path) {
return mediaSource.Path;
}
}
return result.MediaSources[0].Path;
}
else {
return (`error: emby_api ${res.status} ${res.statusText}`);
}
}
catch (error) {
return (`error: emby_api fetch mediaItemInfo failed, ${error}`);
}
}
export default { redirect2Pan };
docker-compose up -d
启动即可,默认暴露端口是8095,如果无法正常访问docker-compose logs -f
查看日志