1331 字
7 分钟
手搓Emby直链播放
2024-10-21

组了台NAS,但是说是网络附加存储,其实小型服务器更准确些。

只配了一块2T的固态用于存储系统数据,电影番剧都是存网盘,毕竟一块24T的红盘都够开15年网盘会员的了。

而且在外使用的时候,家宽的小水管上传根本不够看4K的。

所以这里只要手搓一点点代码就可以实现直链播放。

原理部分#

  • 媒体文件都存储在网盘,本地使用strm文件内存储对应的媒体文件的URL。

  • strm文件内的媒体文件URL可以被Emby正常访问。

  • nginx反向代理Emby服务器,添加strm地址转换接口,并使用njs处理视频接口。

  • 当访问URL时,通过njs获取真实地址并重定向。

需要注意的点#

  • 在Emby的网络设置中,将局域网IP与端口改为nginx反向代理后的地址与端口,否则会自动跳转原始服务器。

  • 如果不能直链播放,请检查是否正在播放转码后的视频。

实现过程#

emby.conf

js_shared_dict_zone zone=jsEmbyCacheDict:10M timeout=2h evict;
server {
    set $EMBY_SERVER_URL "http://192.168.3.3:7096";
    js_import emby from /etc/nginx/conf.d/emby.js;
    listen 80;
    listen 8096;
    server_name emby.nas.local;
    js_fetch_verify off;

    location ~ /(socket|embywebsocket) {
        proxy_pass $EMBY_SERVER_URL;
        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(/.*)$ {
        internal;
        rewrite ^(.*)/proxy(/.*)$ $1$2 break;
        proxy_pass $EMBY_SERVER_URL;
    }

    location ~ ^(.*)/strm(/.*)$ {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        js_content emby.strmHandler;
    }

    location ~* /videos/(.*)/Subtitles {
        js_content emby.apiHandler;
    }

    location ~* /videos/(.*)/(stream|original) {
        js_content emby.apiHandler;
    }

    location ~* /Items/([^/]+)/Download {
        js_content emby.apiHandler;
    }

    location ~* /Sync/JobItems/(.*)/File {
        js_content emby.apiHandler;
    }

    location / {
        proxy_pass $EMBY_SERVER_URL;
    }
}

nginx的配置文件,添加了一个/strm/*路由,用于处理strm文件的链接

视频文件将会由njs的apiHandler函数处理

emby.js

import qs from 'querystring';

const debug = (msg) => {
    ngx.log(ngx.ERR, `DEBUG:${msg}\n`);
}

const EMBY_SERVER_URL = "http://192.168.3.3:7096"
const EMBY_API_KEY = "34fb345c***********8ad9390b"
const WBEDAV_AUTHORIZATION = "Basic MTU***********************************mM3Y="
const URL_HEADER = "http://emby.nas.local/strm/"
const ALIST_URL = "http://alist.nas.local/d/"

const cacheGet = (key) => {
    return ngx.shared.jsEmbyCacheDict.get(key);
}

const cacheSet = async (key, value) => {
    if (!key || !value) {
        debug("!key || !value  RETURN");
        return;
    }
    const dict = ngx.shared["jsEmbyCacheDict"];
    dict.delete(key)
    // dict.add(key, value , 1000);
    dict.add(key, value , 1000 * 60 * 2);
}

const get123PanDirectUrl = async (path) => {
    const pan123Path = `https://webdav-18********59.pd1.123pan.cn/webdav${path}`
    debug(`123pan获取URL中 : ${path}\n ${encodeURI(pan123Path)}`)
    const resp = await ngx.fetch(encodeURI(pan123Path), {
        headers: {
            Authorization: WBEDAV_AUTHORIZATION,
            Range: 'bytes=0-0'
        }
    })
    debug(`123pan获取URL成功 ${path} ==302==> ${resp.headers.Location}`)
    return resp.headers.Location
}

const getAlistDirectUrl = async (path) => {
    const alistUrl = `${ALIST_URL}${path}`
    debug(`alist获取URL中 ${path} ${encodeURI(alistUrl)}`)
    const resp = await ngx.fetch(encodeURI(alistUrl), {
        headers: {
            Range: 'bytes=0-0'
        }
    })
    debug(`alist获取URL成功 ${path} ==302==> ${resp.headers.Location}`)
    return resp.headers.Location
}

const strm2realUrl = async (relativeUrl) => {
    // relativeUrl , 完整的STRM地址去掉URL_HEADER的部分
    // 例如 /123pan/asda/adasd/asda , /xiaoya/asdasd/asdasd/asd
    debug(`strm转真实url:${relativeUrl}`)
    const cachedUrl = cacheGet(relativeUrl)
    if (!cachedUrl) {
        debug(`缓存MISS !`)
        if (relativeUrl.indexOf("/123pan/") === 0) {
            const url = await get123PanDirectUrl(relativeUrl.slice(7))
            cacheSet(relativeUrl, url)
            return url
        } else if (relativeUrl.indexOf("/alist/") === 0) {
            const url = await getXiaoYaDirectUrl(relativeUrl.slice(7))
            cacheSet(relativeUrl, url)
            return url
        } else {
            return null
        }
    } else {
        debug(`缓存命中 ${relativeUrl} ==302==> ${cachedUrl}`)
        return cachedUrl
    }
}

const strmHandler = async (r) => {
    const url = await strm2realUrl(r.uri.replace("/strm/", "/"))
    r.return(302, url)
}


const apiHandler = async (r) => {
    const proxy = `/proxy${r.uri}?1=1&${qs.stringify(r.args)}`
    let match = r.uri.toLowerCase().match(/videos\/(.*)\/(stream|original)/)
    if (match === null) {
        match = r.uri.toLowerCase().match(/items\/([^/]+)\/download/)
    }
    if (match === null) {
        match = r.uri.toLowerCase().match(/sync\/jobitems\/(.*)\/file/)
    }
    if (match === null) {
        debug(`match == null [${r.uri}]`)
        r.internalRedirect(proxy)
    } else {
        const itemInfo = await (await ngx.fetch(`${EMBY_SERVER_URL}/Items?Ids=${match[1]}&Fields=Path,MediaSources&Limit=1&api_key=${EMBY_API_KEY}`)).json()
        const media = itemInfo["Items"][0]["MediaSources"][0]
        if (media["Protocol"] == "Http" && media["Path"].indexOf(URL_HEADER) === 0) {
            const url = await strm2realUrl(media["Path"].replace(URL_HEADER, "/"))
            r.return(302, url)
        } else {
            debug(`本地文件 ${media["Path"]}`)
            r.internalRedirect(proxy)
        }
    }

}

export default { apiHandler, strmHandler };

njs文件,实现了具体的功能。

apiHandler接管请求,向emby服务器请求信息,如果是视频且类型为http,则将strm文件内链接获取到302的地址返回,否则使用内部路由/proxy/来反向代理原始服务器。

以一个strm文件为例:

Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym.strm

在emby详情页选择下载,原始URL为:

http://emby.nas.local/emby/Items/22176/Download?api_key=34fb345c***********8ad9390b&mediaSourceId=mediasource_22176

apiHandler请求emby的Items接口:

http://emby.nas.local/Items?Ids=22176&Fields=Path,MediaSources&Limit=1&api_key=34fb345c***********8ad9390b

取得以下数据:

{
    "Items": [
        {
            "Name": "前目的地",
            "ServerId": "c065f00f3a8844d6b86b5becf9531624",
            "Id": "22176",
            "Container": "mkv",
            "MediaSources": [
                {
                    "Chapters": [],
                    "Protocol": "Http",
                    "Id": "mediasource_22176",
                    "Path": "http://emby.nas.local/strm/123pan/电影/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym.mkv",
                    "Type": "Default",
                    "Container": "mkv",
                    "Size": 11989856916,
                    "Name": "前目的地",
                    "IsRemote": true,
                    "HasMixedProtocols": false,
                    "RunTimeTicks": 58688650000,
                    "SupportsTranscoding": true,
                    "SupportsDirectStream": true,
                    "SupportsDirectPlay": true,
                    "IsInfiniteStream": false,
                    "RequiresOpening": false,
                    "RequiresClosing": false,
                    "RequiresLooping": false,
                    "SupportsProbing": false,
                    "MediaStreams": [
                        {
                            "Codec": "h264",
                            "Language": "eng",
                            "TimeBase": "1/1000",
                            "VideoRange": "SDR",
                            "DisplayTitle": "1080p H264",
                            "DisplayLanguage": "English",
                            "NalLengthSize": "4",
                            "IsInterlaced": false,
                            "BitRate": 16343680,
                            "BitDepth": 8,
                            "RefFrames": 1,
                            "IsDefault": true,
                            "IsForced": false,
                            "IsHearingImpaired": false,
                            "Height": 1080,
                            "Width": 1920,
                            "AverageFrameRate": 23.976025,
                            "RealFrameRate": 23.976025,
                            "Profile": "High",
                            "Type": "Video",
                            "AspectRatio": "16:9",
                            "Index": 0,
                            "IsExternal": false,
                            "IsTextSubtitleStream": false,
                            "SupportsExternalStream": false,
                            "Protocol": "File",
                            "PixelFormat": "yuv420p",
                            "Level": 41,
                            "IsAnamorphic": false,
                            "ExtendedVideoType": "None",
                            "ExtendedVideoSubType": "None",
                            "ExtendedVideoSubTypeDescription": "None",
                            "AttachmentSize": 0
                        },
                        {
                            "Codec": "dts",
                            "Language": "eng",
                            "TimeBase": "1/1000",
                            "DisplayTitle": "English DTS-HD MA 5.1 (默认)",
                            "DisplayLanguage": "English",
                            "IsInterlaced": false,
                            "ChannelLayout": "5.1",
                            "BitDepth": 16,
                            "Channels": 6,
                            "SampleRate": 48000,
                            "IsDefault": true,
                            "IsForced": false,
                            "IsHearingImpaired": false,
                            "Profile": "DTS-HD MA",
                            "Type": "Audio",
                            "Index": 1,
                            "IsExternal": false,
                            "IsTextSubtitleStream": false,
                            "SupportsExternalStream": false,
                            "Protocol": "File",
                            "ExtendedVideoType": "None",
                            "ExtendedVideoSubType": "None",
                            "ExtendedVideoSubTypeDescription": "None",
                            "AttachmentSize": 0
                        },
                        {
                            "Codec": "ass",
                            "Language": "zh-CN",
                            "DisplayTitle": "Chinese Simplified (ASS)",
                            "DisplayLanguage": "Chinese Simplified",
                            "IsInterlaced": false,
                            "IsDefault": false,
                            "IsForced": false,
                            "IsHearingImpaired": false,
                            "Type": "Subtitle",
                            "Index": 21,
                            "IsExternal": true,
                            "IsTextSubtitleStream": true,
                            "SupportsExternalStream": true,
                            "Path": "/mnt/user/storage/Media/EmbyMedia/123pan/电影/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym.chs.ass",
                            "Protocol": "File",
                            "ExtendedVideoType": "None",
                            "ExtendedVideoSubType": "None",
                            "ExtendedVideoSubTypeDescription": "None",
                            "AttachmentSize": 0
                        }
                    ],
                    "Formats": [],
                    "Bitrate": 16343680,
                    "RequiredHttpHeaders": {},
                    "AddApiKeyToDirectStreamUrl": false,
                    "ReadAtNativeFramerate": false,
                    "ItemId": "22176"
                }
            ],
            "Path": "/mnt/user/storage/Media/EmbyMedia/123pan/电影/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym.strm",
            "RunTimeTicks": 58688650000,
            "Size": 11989856916,
            "Bitrate": 16343680,
            "IsFolder": false,
            "Type": "Movie",
            "ImageTags": {
                "Primary": "00db47f51b81c2680f1e3d7986faf9e6",
                "Logo": "2b74dcda7c3ad35227937fa0c2d7ec68",
                "Art": "69818ff598c06b17eb6acff1a23ef787",
                "Banner": "6f36fa4b027ffd4d10b9bcf2acd39370",
                "Thumb": "bdc5c80bcd149d5a2d9867cb3903b4f6"
            },
            "BackdropImageTags": [
                "cacd9ee28056210e53b46016fc895370"
            ],
            "MediaType": "Video"
        }
    ],
    "TotalRecordCount": 1
}

Protocol等于Http,则Path是strm文件内的url。

去除URL_HEADER部分,根据开头判断这是一个123盘的文件链接,绝对地址为:

/电影/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym/Predestination.2014.Bluray.1080p.DTS-HD.x264-Grym.mkv

使用get123PanDirectUrl函数发送请求,在headers中添加Range: 'bytes=0-0'来获取302后的地址。

将获取到的地址使用r.return(302, url)返回,客户端在点击下载时,会跳转直链。alist同理。

自此,通过nginx访问反向代理后的emby,即可实现直链播放。

扩展#

创建strm映射的脚本

RiderLty
/
rcloneStrm
Waiting for api.github.com...
00K
0K
0K
Waiting...
手搓Emby直链播放
https://blog.rd5isto.org/posts/手搓emby直链播放/
作者
Lty
发布于
2024-10-21
许可协议
CC BY-NC-SA 4.0