前言
最近在扩展自己的动漫库,发现Emby和Jellyfin都支持strm这种文件格式,即把视频文件的直链写入strm中,在播放直接调用视频直链进行播放,试了一下这个方案,非常香!
- 节省了服务端的本地空间,目前约17Tb的云盘数据,转换为strm文件的方式后本地媒体数据仅2G左右。
- 相对于我之前使用rclone挂载云盘到VPS路径,刮削速度大大加快,同时也避免了频繁调用导致的网盘API爆炸。
但实际操作过程中,又发现一些大问题
- [x] Emby在播放strm文件时,是通过服务端中转流量的,这一点官方已经给了明确的答复,因为应用商店的审核,视频流必须要经过服务端,这让我3M的小水管情何以堪。
- [x] Jellyfin在播放strm文件时确实是走客户端,但是TV端的解码有点问题,同样的视频在win端和Android端都没有问题,但是在TV端播放失败,查了半天没有解决问题,很蛋疼,放弃Jellyfin的方案。
最终决定使用nginx反代Emby服务端,将视频流转为strm中的直链,客户端加载直链并解码。
Strm文件制作
目前GitHub上有很多成熟的项目了,功能繁多
自己的需求比较简单,简单用Python写了一个,通过alist生成包含strm文件的本地
需要安装对应库
pip install webdavclient3
新建.py文件,输入下面代码并运行
from webdav3.client import Client
import os
import time
folder_from = "/shi/emby/ACG/" # 需要修改的文件夹在alist中的路径,自行修改
folder_to = r'/emby/anime/' # 生成文件的目标路径,自行修改
allow_file_type = ['.nfo','.jpg','.png','.ass','.srt']
video_file_type = [".mkv", ".mp4"]
alist_url = "http://192.168.1.10:5244" # alist的地址,自行修改
# 处理文件夹路径
folder_from = folder_from.replace("\\", "/")
folder_to = folder_to.replace("\\", "/")
if not folder_from.endswith('/'):
folder_from += '/'
if not folder_to.endswith('/'):
folder_to += '/'
options = {
'webdav_hostname': "http://192.168.1.10:5244/dav/", # 修改为alist的webdav地址
'webdav_login': "xxxx", # webdav账号
'webdav_password': "xxxx" # webdav密码
}
client = Client(options)
def start_mask(file):
print(file)
to_file_path = str(file['path']).replace(folder_from,folder_to)
# 判断目标文件夹是否存在该文件
if os.path.exists(to_file_path):
return
else:
# 获取文件后缀
file_extension = os.path.splitext(file['path'])[1]
if file_extension in allow_file_type:
# 获取文件的父文件夹路径
parent_dir = os.path.dirname(to_file_path)
# 检查父文件夹是否存在,如果不存在则递归创建所需的所有文件夹
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
client.download_sync(remote_path=file['path'], local_path=to_file_path)
time.sleep(0.2)
elif file_extension in video_file_type:
strm_path = os.path.splitext(to_file_path)[0] + ".strm"
strm_url = alist_url + "/d" + file['path']
# 获取文件的父文件夹路径
parent_dir = os.path.dirname(strm_path)
# 检查父文件夹是否存在,如果不存在则递归创建所需的所有文件夹
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
with open(strm_path, 'w') as strmfile:
strmfile.write(strm_url)
strmfile.close()
# 遍历文件夹的目录
def get_folderlist(folder_path):
file_list = []
folder_items = client.list(folder_path, get_info=True)
for item in folder_items:
item['path'] = str(item['path']).replace("/dav","")
if item['path'] == folder_path:
continue
if item['isdir']:
get_folderlist(item['path'])
else:
start_mask(item)
file_list = get_folderlist(folder_path=folder_from)
Nginx反向代理
需要使用Nginx的njs模块对拦截的视频流链接进行处理,因为在宿主机环境安装模块比较麻烦,选择使用Nginx的docker容器进行反代,相关配置直接抄现有的轮子魔改。
Nginx的docker容器需要先拷贝出容器中的配置,否则直接映射路径会无法创建配置文件(奇葩设计),计划反向代理的端口为2334。
先创建默认容器
docker run -d --name=nginx -p 2334:2334 --restart=unless-stopped library/nginx
复制容器的配置到宿主机下
docker cp nginx:/etc/nginx/ /root/
此时/root下会有一个nginx文件夹,修改文件夹下的nginx.conf ,在首行添加如下内容:
load_module modules/ngx_http_js_module.so;
修改/root/nginx/conf.d/default.conf文件,修改为以下内容
# Load the njs script
js_path /etc/nginx/conf.d/;
js_import emby2Pan from emby.js;
#Cache images
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
{
gzip on;
listen 2334; #反代后的端口,域名为80或443
server_name 192.168.1.10; #修改为自己的IP或域名
add_header 'Referrer-Policy' 'no-referrer';
set $emby http://192.168.1.10:8096; #修改为自己的emby地址
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;
}
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;
}
location ~* /videos/(\d+)/(original\.(mkv|mp4|avi|mov|wmv|mp3|wav|ogg|strm)) {
js_content emby2Pan.redirect2Pan;
}
location ~* /Items/(\d+)/Download {
js_content emby2Pan.redirect2Pan;
}
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;
}
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;
}
}
在conf.d文件夹下新建emby.js文件,并填入下面的内容
async function redirect2Pan(r) {
const embyHost = 'http://127.0.0.1:8096'; //emby的地址
const api_key = 'b3ae5d1a7ada43cc942e92fb02c14534'; //emby的API,用于获取信息
const itemId = /[\d]+/.exec(r.uri)[0];
const mediaSourceId = r.args.MediaSourceId;
const itemInfoUri = `${embyHost}/emby/Items/${itemId}/PlaybackInfo?api_key=${api_key}`;
r.error(`itemInfoUri: ${itemInfoUri}`);
const video_path= await fetchEmbyFilePath(itemInfoUri, mediaSourceId);
r.error(`mount emby file path: ${video_path}`);
r.return(302, video_path);
return;
}
async function fetchEmbyFilePath(itemInfoUri, mediaSourceId) {
try {
const res = await ngx.fetch(itemInfoUri, { 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`;
}
const mediaSource = result.MediaSources.find(m => m.Id == mediaSourceId);
if (mediaSource === null || mediaSource === undefined) {
return `error: emby_api mediaSourceId ${mediaSourceId} not found`;
}
return mediaSource.Path;
}
else {
return (`error: emby_api ${res.status} ${res.statusText}`);
}
}
catch (error) {
return (`error: emby_api fetch mediaItemInfo failed, ${error}`);
}
}
export default { redirect2Pan };
将配置覆盖回容器并重启容器
docker cp /root/nginx nginx:/etc
docker restart nginx
最终效果
测试打开反代地址正常,Emby在关闭用户转码权限后成功由客户端进行直链加载和解码。
8 条评论
真棒!
兄弟写的非常好 https://www.cscnn.com/
看的我热血沸腾啊https://www.ea55.com/
怎么收藏这篇文章?
看的我热血沸腾啊https://www.jiwenlaw.com/
叼茂SEO.bfbikes.com
博主真是太厉害了!!!
陈尿引:文章真不错http://keah.1ut9wd.cn