前言

最近在扩展自己的动漫库,发现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上有很多成熟的项目了,功能繁多

Readme Card

Readme Card

自己的需求比较简单,简单用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容器进行反代,相关配置直接抄现有的轮子魔改。

Readme Card

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在关闭用户转码权限后成功由客户端进行直链加载和解码。

最后修改:2024 年 06 月 25 日
如果觉得我的文章对你有用,请随意赞赏