目录
背景介绍
通过 Nginx 的 njs 模块实现 Emby 视频请求直接重定向到 OneDrive 直链,避免服务器转码和流量消耗。
环境要求
Nginx with njs moduleEmby ServerAlistOneDrive 存储
实现原理
Emby 发起视频请求Nginx 拦截请求并通过 njs 处理获取 Alist 直链302 重定向到 OneDrive 直链客户端直接从 OneDrive 获取视频数据
更多配置请参考大佬文章:https://syq.pub/archives/27/
配置步骤
1. Nginx 配置
在 /etc/nginx/conf.d/emby.conf 中添加:
js_path /etc/nginx/conf.d/;
js_import emby2Pan from emby.js;
proxy_cache_path /var/cache/nginx/emby levels=1:2 keys_zone=emby:100m max_size=1g inactive=30d use_temp_path=off;proxy_cache_path /var/cache/nginx/emby/subs levels=1:2 keys_zone=embysubs:10m max_size=1g inactive=30d use_temp_path=off;
server {
listen 80;
server_name 你的emby域名;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy 'no-referrer';
set $emby http://127.0.0.1:8096;
# 修改视频匹配规则,同时匹配直接播放和 HLS 格式
location ~* /emby/videos/(\d+)/(original\..*|hls1/.*\.ts|hls1/.*\.m3u8|hls1/.*\.mp4) {
js_content emby2Pan.redirect2Pan;
}
location ~ /(socket|embywebsocket) {
proxy_pass $emby;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
keepalive_timeout 86400;
}
location ~* /videos/(.*)/Subtitles {
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_cache embysubs;
proxy_cache_revalidate on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock on;
proxy_cache_valid 200 30d;
proxy_cache_key $proxy_host$uri;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
}
location ~ /Items/(.*)/Images {
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_cache emby;
proxy_cache_revalidate on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock on;
proxy_cache_valid 200 30d;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_cache_key $proxy_host$uri;
}
location / {
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_buffering off;
}
}
2. njs 脚本
async function redirect2Pan(r) {
r.warn('===============开始处理视频请求===============');
r.warn('请求URI: ' + r.uri);
r.warn('请求参数: ' + JSON.stringify(r.args));
const config = {
embyHost: 'http://127.0.0.1:8096',
embyMountPath: '/mnt/',
alistHost: 'http://127.0.0.1:5244',
alistUsername: 'admin',
alistPassword: 'alist登录密码',
defaultApiKey: 'emby密钥'
};
try {
// 从 Emby 请求中提取视频 ID
const videoId = r.uri.match(/\/emby\/videos\/(\d+)\//)[1];
// 获取视频信息
const videoInfoRes = await ngx.fetch(`${config.embyHost}/emby/Items/${videoId}/PlaybackInfo?api_key=${config.defaultApiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const videoInfo = await videoInfoRes.json();
if (!videoInfo.MediaSources || !videoInfo.MediaSources[0] || !videoInfo.MediaSources[0].Path) {
throw new Error('无法获取视频文件路径');
}
const embyPath = videoInfo.MediaSources[0].Path;
r.warn('获取到 Emby 文件路径: ' + embyPath);
// 处理路径中的特殊字符
let alistPath = embyPath.replace(config.embyMountPath, '/');
// 替换冒号为全角冒号
alistPath = alistPath.replace(/:/g, ':');
r.warn('Alist 文件路径: ' + alistPath);
// 获取 Alist token
const loginRes = await ngx.fetch(config.alistHost + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: config.alistUsername,
password: config.alistPassword
})
});
const loginData = await loginRes.json();
if (loginData.code !== 200) {
throw new Error('登录失败: ' + loginData.message);
}
// 获取文件直链
const getRes = await ngx.fetch(config.alistHost + '/api/fs/get', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': loginData.data.token
},
body: JSON.stringify({
path: alistPath,
password: '',
page: 1,
per_page: 0,
refresh: false
})
});
const getData = await getRes.json();
r.warn('获取文件信息响应: ' + JSON.stringify(getData));
if (getData.code !== 200 || !getData.data.raw_url) {
throw new Error('获取文件直链失败: ' + getData.message);
}
r.warn('获取到 Alist 直链: ' + getData.data.raw_url);
// 添加必要的响应头
r.headersOut['Access-Control-Allow-Origin'] = '*';
r.headersOut['Access-Control-Allow-Methods'] = 'GET, OPTIONS';
r.headersOut['Access-Control-Allow-Headers'] = 'Range';
r.headersOut['Accept-Ranges'] = 'bytes';
return r.return(302, getData.data.raw_url);
} catch (error) {
r.error('处理请求时发生错误: ' + error.toString());
return r.return(500, 'Internal Server Error');
}
}
export default { redirect2Pan };
cat /etc/nginx/nginx.conf 配置参考:
load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;
user www-data;
worker_processes auto;
pid /run/nginx.pid;
# 确保设置了 debug 级别
error_log /var/log/nginx/error.log debug;
events {
worker_connections 768;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Emby 设置
1. 服务器端设置:管理 -> 媒体库 -> 播放启用直接播放禁用转码客户端设置:设置 -> 播放播放方式选择”直接播放”关闭”启用 HLS”最大流媒体比特率选择”原始”
验证方法
查看 Nginx 错误日志:
tail -f /var/log/nginx/error.log
正常日志示例:
2025/01/19 00:07:43 [warn] 6640#6640: *432 js: 获取到 Emby 文件路径: /mnt/odmovie1/电影/我是谁.mkv
2025/01/19 00:07:43 [warn] 6640#6640: *432 js: Alist 文件路径: /odmovie1/电影/我是谁.mkv
2025/01/19 00:07:43 [warn] 6640#6640: *432 js: 获取到 Alist 直链: https://xxx.sharepoint.com/…
优势
无需服务器转码不消耗服务器带宽播放速度取决于 OneDrive 速度服务器只处理轻量级请求支持特殊字符文件名
注意事项
确保 Nginx 已安装 njs 模块正确配置 Emby API Key配置正确的 Alist 账号密码路径映射要准确匹配处理文件名中的特殊字符
故障排查
检查 Nginx 错误日志确认 Emby API 可访问验证 Alist 登录状态检查文件路径映射确认客户端播放设置
参考资料
重启 nginx
nginx -t && nginx -s reload && systemctl restart nginx
tail -f /var/log/nginx/error.log