Open Akimio521 opened 4 months ago
看到了constant-transcode.js
,配置了一下web可以正常播放,但是本地转码失效了....
里面的变量名看着有点懵,我说一下我的情况和配置情况
目前我的影视库有三种视频:
目前我配置下面几个地方,
const embyHost = "http://172.17.0.1:8096";
const embyApiKey = "ebbcbxxxxxxxxx9e12f";
const mediaMountPath = ["/media/strm"];
const transcodeConfig = {
enable: true,
enableStrmTranscode: false,
type: "distributed-media-server",
maxNum: 3,
redirectTransOptEnable: true,
targetItemMatchFallback: "redirect",
server: []
};
多谢提供详细的描述
1.配置上没啥问题,不过少了一个,历史遗留问题,路由规则没匹配上在代码中写死是默认 302 了,所以 routeRule 需要手动指定下允许转码的文件路径开头
// 路由规则,注意有先后顺序,"proxy"规则优先级最高,其余依次,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内
// 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1
// "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链302, "transcode": 转码, "block": 只是屏蔽播放
// "transcode",稍微有些歧义,大部分情况等同于"proxy",这里只是不做转码参数修改,具体是否转码由 emby 客户端自己判断上报或客户端手动切换码率控制
// 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1
// 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接
// 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g)
// 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配)
const routeRule = [
["transcode", "filePath", 0, "/media/local"],
];
写死默认 302 是考虑到服务器压力,还有历史代码的遗留原因,暂时不会改这个默认的逻辑
如果可以把emby2alist集成到NPM的话更好
2.这个的话没法集成,因为目前代码中用到了太多 nginx 特性了,已经强依赖了,毕竟 nginx 反代这块性能和自由度还是很强的,假如需要参照实现的话,可以以这一版或之前的几版对照,当时的代码量很少,但是需要注意当时是以 302 为强制性的, 需要转码的话得手动连接到原始服务上来间接支持
https://github.com/bpking1/embyExternalUrl/commit/932b1077016ad29da2535392cf7f027d717821a4
EmbyServer:host Emby-Nginx:bridge Nginx-Proxy-Manager:brigde 所有访问都通过公网URL进行访问,因为最前面的NPM的网络是bridge,所以EmbyServer看到所有的访问都是私有地址
3.这个只需要 nginx 之前的反代程序传递一些默认约定俗成的代理标识头就行了,可参照 emby2Alist\nginx\conf.d\includes\proxy-header.conf 或 alist 官方文档中也有提到的反代注意点, NPM 反代的话我这边没用过,不知道群晖是不是也是这样可自定义的,忽略截图中的反代项目和此问题无关,只是配置样式举例
感觉整个项目有很大一部分在处理关于CD2映射出来的本地软链接,其实这一部分应该由其他程序处理的,个人觉得不太适合放在alist2emby这个nginx应用里,这样会有太多无意义的字符处理过程
可能我对
const mediaMountPath = ["/mnt"];
对理解错误了,我以为只有/mnt
目录下会交给该程序处理,其他的会原封不动转发,所以我就没有写那个routeRule的配置,所以我这里继续保持/media/strm
吗
另外关于那个routeRule我还有一个疑惑,redirect
是指直接返回alist的那个有/d/
的下载url还是返回通过Alist的API:/api/fs/get
得到的raw_url
感觉整个项目有很大一部分在处理关于CD2映射出来的本地软链接
1.这个倒不是,软链接其实只有 check symlinkRule 这几行,看起来路径处理太多是因为兼容了多种形式的,软链接,STRM,路径映射以达到重定向至其它 alist 或 cd 访问链接 或 其它直链提供程序的访问链接上,当然最初是作为 mediaMountPath 的增强来用的,因为部分用户的路径挂载千奇百怪,所以开放了一个自由度更高的参数 mediaPathMapping 让用户自己做路径处理了
以为只有/mnt目录下会交给该程序处理,其他的会原封不动转发
2.确实是理解错了,初版脚本也是做的多出来的这级挂载工具特有的路径做移除的
3.可以继续保持 /media/strm ,这样不影响其它文件的原始路径
4.routeRule 中的 redirect 特指的是此脚本中的一种行为,当然也是等价为返回通过 Alist 的 API:/api/fs/get 得到的 raw_url ,
返回alist的那个有/d/的下载url
5.提到的这个,是通过这个参数来进行控制的, emby2Alist\nginx\conf.d\config\constant-mount.js
// 指定客户端自己请求并获取 alist 直链的规则,代码优先级在 redirectStrmLastLinkRule 之后
// 特殊情况使用,则此处必须使用域名且公网畅通,用不着请保持默认
// 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1
// 参数2: 匹配类型或来源(字符串参数类型),优先级高"filePath": 文件路径(Item.Path),默认为"alistRes": alist 返回的链接 raw_url
// ,有分组时不可省略填写,可为表达式
// 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g)
// 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配)
// 参数5: 指定转发给客户端的 alist 的 host 前缀,兼容 sign 参数
const clientSelfAlistRule = [
// "Emby for iOS"和"Infuse"对于 115 的进度条拖动依赖于此
// 如果 nginx 为 https,则此 alist 也必须 https,浏览器行为客户端会阻止非 https 请求
[2, strHead["115"], alistPublicAddr],
// [2, strHead.ali, alistPublicAddr],
// 优先使用 filePath,可省去一次查询 alist,如驱动为 alias,则应使用 alistRes
// ["115-local", "filePath", 0, "/mnt/115", alistPublicAddr],
// ["115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], // 链接入参,客户端类型
// ["115-alist", "alistRes", 2, strHead["115"], alistPublicAddr],
// ["115-alist", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug],
];
挂载方式还真是千奇百怪,我现在用的是我自己写的一个小程序定时更新strm,签名什么的全部交给我那个程序负责,通过硬链接将strm映射到emby的媒体库中,因为是硬链接,所以我的小程序更新了原始文件的strm,媒体库中的strm也同步更新了,至于为什么要时时更新,主要是因为我直接填的是公网的alist,并且设定了token刷新时间是24小时,这样能保证安全 这样下来对于emby服务器来说就是一个可以直接访问的url,算是让整个服务没有弄的那么复杂吧
顺便问一下,embyserver的相关api在哪里查看,我打算自己写一个更符合我习惯的反代去直接返回客户端直链
感觉整个项目有很大一部分在处理关于CD2映射出来的本地软链接
1.这个倒不是,软链接其实只有 check symlinkRule 这几行,看起来路径处理太多是因为兼容了多种形式的,软链接,STRM,路径映射以达到重定向至其它 alist 或 cd 访问链接 或 其它直链提供程序的访问链接上,当然最初是作为 mediaMountPath 的增强来用的,因为部分用户的路径挂载千奇百怪,所以开放了一个自由度更高的参数 mediaPathMapping 让用户自己做路径处理了
以为只有/mnt目录下会交给该程序处理,其他的会原封不动转发
2.确实是理解错了,初版脚本也是做的多出来的这级挂载工具特有的路径做移除的
3.可以继续保持 /media/strm ,这样不影响其它文件的原始路径
4.routeRule 中的 redirect 特指的是此脚本中的一种行为,当然也是等价为返回通过 Alist 的 API:/api/fs/get 得到的 raw_url ,
返回alist的那个有/d/的下载url
5.提到的这个,是通过这个参数来进行控制的,
emby2Alist\nginx\conf.d\config\constant-mount.js
// 指定客户端自己请求并获取 alist 直链的规则,代码优先级在 redirectStrmLastLinkRule 之后 // 特殊情况使用,则此处必须使用域名且公网畅通,用不着请保持默认 // 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 // 参数2: 匹配类型或来源(字符串参数类型),优先级高"filePath": 文件路径(Item.Path),默认为"alistRes": alist 返回的链接 raw_url // ,有分组时不可省略填写,可为表达式 // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) // 参数5: 指定转发给客户端的 alist 的 host 前缀,兼容 sign 参数 const clientSelfAlistRule = [ // "Emby for iOS"和"Infuse"对于 115 的进度条拖动依赖于此 // 如果 nginx 为 https,则此 alist 也必须 https,浏览器行为客户端会阻止非 https 请求 [2, strHead["115"], alistPublicAddr], // [2, strHead.ali, alistPublicAddr], // 优先使用 filePath,可省去一次查询 alist,如驱动为 alias,则应使用 alistRes // ["115-local", "filePath", 0, "/mnt/115", alistPublicAddr], // ["115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 // ["115-alist", "alistRes", 2, strHead["115"], alistPublicAddr], // ["115-alist", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], ];
如果redirect是访问alist的api获取raw_url,那对于非alist的地址又是如何处理的,例如我本身strm存放的就是raw_url(不考虑网盘cdn的过期时间的前提下)
1.这个是官方在线的,但是不好调试 https://dev.emby.media/reference/RestAPI.html
2.通过服务端 => 设置 => 控制台 ,最下边的 API 超链接访问自己服务端上的 swagger 文档,就可以导入到 API 调试工具里了
如果redirect是访问alist的api获取raw_url,那对于非alist的地址又是如何处理的,例如我本身strm存放的就是raw_url(不考虑网盘cdn的过期时间的前提下)
对于非 / 或 \ 开头的远程链接,都是 nginx 直接响应了链接,但是这里存在一个已知差异点,因为没人 issus 所以没管
这里对远程链接进行了转义解码
// strm file internal text maybe encode
r.warn(`notLocal: ${embyRes.notLocal}`);
if (embyRes.notLocal) {
embyRes.path = decodeURIComponent(embyRes.path);
r.warn(`notLocal decodeURIComponent embyRes.path`);
}
而这里 return 的前几行只对特定工具的路径部分进行了转义编码
// strm file inner remote link redirect,like: http,rtsp
// not only strm, mediaPathMapping maybe used remote link
isRemote = !util.isAbsolutePath(mediaItemPath);
if (isRemote) {
let rule = util.simpleRuleFilter(
r, config.redirectStrmLastLinkRule, mediaItemPath,
util.SOURCE_STR_ENUM.filePath, "redirectStrmLastLinkRule"
);
if (rule && rule.length > 0) {
if (!Number.isInteger(rule[0])) {
// convert groupRule remove groupKey and sourceValue
r.warn(`convert groupRule remove groupKey and sourceValue`);
rule = rule.slice(2);
}
let directUrl = await ngxExt.fetchLastLink(mediaItemPath, rule[2], rule[3], ua);
if (!!directUrl) {
mediaItemPath = directUrl;
} else {
r.warn(`warn: fetchLastLink, not expected result, failback once`);
directUrl = await ngxExt.fetchLastLink(ngxExt.lastLinkFailback(mediaItemPath), rule[2], rule[3], ua);
if (!!directUrl) {
mediaItemPath = directUrl;
}
}
}
// need careful encode filePathPart, other don't encode
const filePathPart = util.getFilePathPart(mediaItemPath);
if (filePathPart) {
r.warn(`is CloudDrive/AList link, encodeURIComponent filePathPart before: ${mediaItemPath}`);
mediaItemPath = mediaItemPath.replace(filePathPart, encodeURIComponent(filePathPart));
}
return redirect(r, mediaItemPath);
}
配置routeRule后Web不能转码本地视频(设置成240p后打开播放统计还是提示原码率直接播放),web也不能正常播放strm 客户端可以302播放strm,客户端转码暂未测试(infuse和filebox似乎没办法直接指定码率)
["transcode", "LocalVideo", "filePath", 0, "/media/local"],
配置routeRule后Web不能转码本地视频(设置成240p后打开播放统计还是提示原码率直接播放)
1.方便提供下日志吗?这样好排查一点
web也不能正常播放strm , 客户端可以302播放strm
2.这个听起来很像是 web 播放 115 链接没有响应允许跨域头的问题,假如是这个问题,可以参考
客户端转码暂未测试(infuse和filebox似乎没办法直接指定码率)
3.官方客户端转码表现形式应该和 web 端差不多,第三方播放器的确是不支持转码的,串流和转码地址是两个分开的响应参数,而第三方播放器都是取的串流直接播放地址,不理会转码链接地址
需要的是nginx的日志吗 我刚刚尝试使用windows客户端转码,似乎也无法正常转码
2.这个听起来很像是 web 播放 115 链接没有响应允许跨域头的问题,假如是这个问题,可以参考
并不是115网盘,是普通的http可以获取的视频
1.是的,一般在 ../nginx-emby2alist/log/error.log 下,如果是 docker 环境,可以看下日志目录挂载到哪里了
volumes:
- ../nginx/nginx.conf:/etc/nginx/nginx.conf
- ../nginx/conf.d:/etc/nginx/conf.d
- ../nginx/embyCache:/var/cache/nginx/emby
- ../nginx/log:/var/log/nginx
并不是115网盘,是普通的http可以获取的视频
2.那这应该就是媒体格式 web 浏览器不支持了,源服务是走转码来兼容的播放,现在合并成一个问题了,能指定正确转码就可以 web 播放了
方便加个联系方式私聊吗
不知道删了邮件还会不会有历史消息,如果没看到可以回一下
不知道删了邮件还会不会有历史消息,如果没看到可以回一下
我的gmail并没有收到邮件
3.可以继续保持 /media/strm ,这样不影响其它文件的原始路径
似乎是没有命中mediaMountPath
的规则,所以默认启用了proxy模式,所以mediaMountPath
应该填strm和local的父路径/media
吗?
2024/07/18 05:04:21 [notice] 23#23: *198 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000000027, client: 172.18.0.22, server: default, request: "POST /emby/Items/11/PlaybackInfo?UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=420000&X-Emby-Client=Emby+Web&X-Emby-Device-Name=Microsoft+Edge+Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json HTTP/1.1", host: "emby.xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
2024/07/18 05:04:21 [warn] 23#23: *198 js: playbackinfo proxy uri: /proxy/emby/Items/11/PlaybackInfo
2024/07/18 05:04:21 [warn] 23#23: *198 js: playbackinfo proxy query string: UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=420000&X-Emby-Client=Emby Web&X-Emby-Device-Name=Microsoft Edge Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json
2024/07/18 05:04:21 [warn] 23#23: *198 js: origin playbackinfo: {"MediaSources":[{"Protocol":"File","Id":"82e6b0cacbd81e7dae7fa5a274af31f8","Path":"/media/local/女神的露天咖啡厅 (2023)/Season 2/女神的露天咖啡厅 S02E01 -1080p -X264 -AAC -DAY.mp4","Type":"Default","Container":"mp4","Size":536521898,"Name":"女神的露天咖啡厅 S02E01 -1080p -X264 -AAC -DAY","IsRemote":false,"HasMixedProtocols":false,"RunTimeTicks":14352016670,"SupportsTranscoding":true,"SupportsDirectStream":false,"SupportsDirectPlay":false,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/24000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":2836404,"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":40,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/44100","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":150704,"Channels":2,"SampleRate":44100,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0}],"Formats":[],"Bitrate":2990642,"RequiredHttpHeaders":{},"DirectStreamUrl":"/videos/11/master.m3u8?DeviceId=e12ab84a-2948-463a-9cb
2024/07/18 05:04:21 [warn] 23#23: *198 js: modify direct play supports all true
2024/07/18 05:04:21 [warn] 23#23: *198 js: hit proxy, not mountPath first: ["/media/strm"]
2024/07/18 05:04:21 [warn] 23#23: *198 js: playbackinfo routeMode: proxy
2024/07/18 05:04:21 [warn] 23#23: *198 js: modify direct play info
2024/07/18 05:04:21 [warn] 23#23: *198 js: 253ms, transfer playbackinfo: {"MediaSources":[{"Protocol":"File","Id":"82e6b0cacbd81e7dae7fa5a274af31f8","Path":"/media/local/女神的露天咖啡厅 (2023)/Season 2/女神的露天咖啡厅 S02E01 -1080p -X264 -AAC -DAY.mp4","Type":"Default","Container":"mp4","Size":536521898,"Name":"女神的露天咖啡厅 S02E01 -1080p -X264 -AAC -DAY","IsRemote":false,"HasMixedProtocols":false,"RunTimeTicks":14352016670,"SupportsTranscoding":true,"SupportsDirectStream":true,"SupportsDirectPlay":true,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/24000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":2836404,"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":40,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/44100","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":150704,"Channels":2,"SampleRate":44100,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0}],"Formats":[],"Bitrate":2990642,"RequiredHttpHeaders":{},"DirectStreamUrl":"/videos/11/stream.mp4?UserId=916c17b8f9f341368e
2024/07/18 05:04:21 [warn] 23#23: *198 js: === transferPlaybackInfo: /emby/Items/11/PlaybackInfo, the NJS VM is destroyed ===
2024/07/18 05:04:22 [warn] 23#23: *202 js: redirect2Pan, UA: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0
2024/07/18 05:04:22 [warn] 23#23: *202 js: itemInfoUri: http://172.17.0.1:8096/Items?Ids=82e6b0cacbd81e7dae7fa5a274af31f8&Fields=Path,MediaSources&Limit=1&api_key=bfc2197dccaf4c3a99e92347c67e275d
2024/07/18 05:04:22 [warn] 23#23: *202 js: 6ms, fetchEmbyFilePath async function cost
2024/07/18 05:04:22 [warn] 23#23: *202 js: notLocal: false
2024/07/18 05:04:22 [warn] 23#23: *202 js: mount emby file path: /media/local/女神的露天咖啡厅 (2023)/Season 2/女神的露天咖啡厅 S02E01 -1080p -X264 -AAC -DAY.mp4
2024/07/18 05:04:22 [warn] 23#23: *202 js: hit proxy, not mountPath first: ["/media/strm"]
2024/07/18 05:04:22 [warn] 23#23: *202 js: getRouteMode: proxy
2024/07/18 05:04:22 [warn] 23#23: *202 js: use original link
似乎是没有命中
mediaMountPath
的规则,所以默认启用了proxy模式,所以mediaMountPath
应该填strm和local的父路径/media
吗?
将mediaMountPath
改为["/media"]
后本地文件转码正常,但是浏览器无法播放Strm视频,ios客户端可以正常302
将mediaMountPath
改为["/media/strm"]
后浏览器还是不能正确播放Strm
2024/07/18 05:37:31 [warn] 21#21: *1161 js: notLocal decodeURIComponent embyRes.path
2024/07/18 05:37:31 [warn] 21#21: *1161 js: mount emby file path: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:31 [warn] 21#21: *1161 js: sourceStrValue, LocalVideo = [object Request]
2024/07/18 05:37:31 [warn] 21#21: *1161 js: getRouteMode: redirect
2024/07/18 05:37:31 [warn] 21#21: *1161 js: mediaPathMapping: [[0,0,"/media/strm",""]]
2024/07/18 05:37:31 [warn] 21#21: *1161 js: mapped emby file path: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:31 [warn] 21#21: *1161 js: redirect to: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:31 [warn] 21#21: *1161 js: routeL1Dict add: [/emby/videos/10626/stream.mp4:a4f74d141680f9fa3a262f5f59ac361f] : [https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true]
2024/07/18 05:37:31 [warn] 21#21: *1161 js: === redirect2Pan: /emby/videos/10626/stream.mp4, the NJS VM is destroyed ===
2024/07/18 05:37:33 [notice] 20#20: *1167 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000000151, client: 172.18.0.22, server: default, request: "POST /emby/Items/10626/PlaybackInfo?UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&AudioStreamIndex=1&SubtitleStreamIndex=-1&EnableDirectPlay=false&EnableDirectStream=false&MediaSourceId=a4f74d141680f9fa3a262f5f59ac361f&MaxStreamingBitrate=320000&CurrentPlaySessionId=0d47f97ab837423d9ac6b5d6d3dacb47&X-Emby-Client=Emby+Web&X-Emby-Device-Name=Microsoft+Edge+Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json HTTP/1.1", host: "emby.xxxxxxxxxxxxxxxx"
2024/07/18 05:37:33 [warn] 20#20: *1167 js: playbackinfo proxy uri: /proxy/emby/Items/10626/PlaybackInfo
2024/07/18 05:37:33 [warn] 20#20: *1167 js: playbackinfo proxy query string: UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&AudioStreamIndex=1&SubtitleStreamIndex=-1&EnableDirectPlay=false&EnableDirectStream=false&MediaSourceId=a4f74d141680f9fa3a262f5f59ac361f&MaxStreamingBitrate=320000&CurrentPlaySessionId=0d47f97ab837423d9ac6b5d6d3dacb47&X-Emby-Client=Emby Web&X-Emby-Device-Name=Microsoft Edge Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json
2024/07/18 05:37:33 [warn] 20#20: *1167 js: origin playbackinfo: {"MediaSources":[{"Protocol":"Http","Id":"a4f74d141680f9fa3a262f5f59ac361f","Path":"https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true","Type":"Default","Container":"mp4","Size":353189084,"Name":"魔王军最强的魔术师是人类 S01E01 -1080p -AVC -AAC -ANi -Baha","IsRemote":true,"HasMixedProtocols":false,"RunTimeTicks":14200750000,"SupportsTranscoding":true,"SupportsDirectStream":false,"SupportsDirectPlay":false,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/90000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":1684283,"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":50,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/48000","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":298259,"Channels":2,"SampleRate":48000,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"mjpeg","ColorSpace":"bt470bg","TimeBase":"1/90000","IsInterlaced":false,"BitDepth":8,"RefFrames"
2024/07/18 05:37:33 [warn] 20#20: *1167 js: modify direct play supports all true
2024/07/18 05:37:33 [warn] 20#20: *1167 js: sourceStrValue, LocalVideo = [object Request]
2024/07/18 05:37:33 [warn] 20#20: *1167 js: playbackinfo routeMode: redirect
2024/07/18 05:37:33 [warn] 20#20: *1167 js: modify direct play info
2024/07/18 05:37:33 [warn] 20#20: *1167 js: 5ms, transfer playbackinfo: {"MediaSources":[{"Protocol":"Http","Id":"a4f74d141680f9fa3a262f5f59ac361f","Path":"https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true","Type":"Default","Container":"mp4","Size":353189084,"Name":"魔王军最强的魔术师是人类 S01E01 -1080p -AVC -AAC -ANi -Baha","IsRemote":true,"HasMixedProtocols":false,"RunTimeTicks":14200750000,"SupportsTranscoding":true,"SupportsDirectStream":true,"SupportsDirectPlay":true,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/90000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":1684283,"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":50,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/48000","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":298259,"Channels":2,"SampleRate":48000,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"mjpeg","ColorSpace":"bt470bg","TimeBase":"1/90000","IsInterlaced":false,"BitDepth":8,"RefFr
2024/07/18 05:37:33 [warn] 20#20: *1167 js: === transferPlaybackInfo: /emby/Items/10626/PlaybackInfo, the NJS VM is destroyed ===
2024/07/18 05:37:33 [warn] 20#20: *1171 js: redirect2Pan, UA: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0
2024/07/18 05:37:33 [warn] 20#20: *1171 js: hit cache routeL1Dict: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:33 [warn] 20#20: *1171 js: redirect to: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:33 [warn] 20#20: *1171 js: === redirect2Pan: /emby/videos/10626/stream.mp4, the NJS VM is destroyed ===
2024/07/18 05:37:34 [notice] 20#20: *1174 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000000152, client: 172.18.0.22, server: default, request: "POST /emby/Items/10626/PlaybackInfo?UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&AudioStreamIndex=1&SubtitleStreamIndex=-1&EnableDirectPlay=false&EnableDirectStream=false&MediaSourceId=a4f74d141680f9fa3a262f5f59ac361f&MaxStreamingBitrate=320000&CurrentPlaySessionId=e4c4a68a89c0492ca26f8d1ea6608751&X-Emby-Client=Emby+Web&X-Emby-Device-Name=Microsoft+Edge+Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json HTTP/1.1", host: "emby.xxxxxxxxxxxxxxxxxx"
2024/07/18 05:37:34 [warn] 20#20: *1174 js: playbackinfo proxy uri: /proxy/emby/Items/10626/PlaybackInfo
2024/07/18 05:37:34 [warn] 20#20: *1174 js: playbackinfo proxy query string: UserId=916c17b8f9f341368e6d9d310dc83868&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&AudioStreamIndex=1&SubtitleStreamIndex=-1&EnableDirectPlay=false&EnableDirectStream=false&MediaSourceId=a4f74d141680f9fa3a262f5f59ac361f&MaxStreamingBitrate=320000&CurrentPlaySessionId=e4c4a68a89c0492ca26f8d1ea6608751&X-Emby-Client=Emby Web&X-Emby-Device-Name=Microsoft Edge Windows&X-Emby-Device-Id=e12ab84a-2948-463a-9cb0-ecebcac2949e&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=bfc2197dccaf4c3a99e92347c67e275d&X-Emby-Language=zh-cn&reqformat=json
2024/07/18 05:37:34 [warn] 20#20: *1174 js: origin playbackinfo: {"MediaSources":[{"Protocol":"Http","Id":"a4f74d141680f9fa3a262f5f59ac361f","Path":"https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true","Type":"Default","Container":"mp4","Size":353189084,"Name":"魔王军最强的魔术师是人类 S01E01 -1080p -AVC -AAC -ANi -Baha","IsRemote":true,"HasMixedProtocols":false,"RunTimeTicks":14200750000,"SupportsTranscoding":true,"SupportsDirectStream":false,"SupportsDirectPlay":false,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/90000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":1684283,"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":50,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/48000","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":298259,"Channels":2,"SampleRate":48000,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"mjpeg","ColorSpace":"bt470bg","TimeBase":"1/90000","IsInterlaced":false,"BitDepth":8,"RefFrames"
2024/07/18 05:37:34 [warn] 20#20: *1174 js: modify direct play supports all true
2024/07/18 05:37:34 [warn] 20#20: *1174 js: sourceStrValue, LocalVideo = [object Request]
2024/07/18 05:37:34 [warn] 20#20: *1174 js: playbackinfo routeMode: redirect
2024/07/18 05:37:34 [warn] 20#20: *1174 js: modify direct play info
2024/07/18 05:37:34 [warn] 20#20: *1174 js: 5ms, transfer playbackinfo: {"MediaSources":[{"Protocol":"Http","Id":"a4f74d141680f9fa3a262f5f59ac361f","Path":"https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true","Type":"Default","Container":"mp4","Size":353189084,"Name":"魔王军最强的魔术师是人类 S01E01 -1080p -AVC -AAC -ANi -Baha","IsRemote":true,"HasMixedProtocols":false,"RunTimeTicks":14200750000,"SupportsTranscoding":true,"SupportsDirectStream":true,"SupportsDirectPlay":true,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"h264","CodecTag":"avc1","Language":"und","TimeBase":"1/90000","VideoRange":"SDR","DisplayTitle":"1080p H264","NalLengthSize":"4","IsInterlaced":false,"BitRate":1684283,"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":50,"IsAnamorphic":false,"ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"aac","CodecTag":"mp4a","Language":"jpn","TimeBase":"1/48000","DisplayTitle":"Japanese AAC stereo (默认)","DisplayLanguage":"Japanese","IsInterlaced":false,"ChannelLayout":"stereo","BitRate":298259,"Channels":2,"SampleRate":48000,"IsDefault":true,"IsForced":false,"IsHearingImpaired":false,"Profile":"LC","Type":"Audio","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","ExtendedVideoType":"None","ExtendedVideoSubType":"None","ExtendedVideoSubTypeDescription":"None","AttachmentSize":0},{"Codec":"mjpeg","ColorSpace":"bt470bg","TimeBase":"1/90000","IsInterlaced":false,"BitDepth":8,"RefFr
2024/07/18 05:37:34 [warn] 20#20: *1174 js: === transferPlaybackInfo: /emby/Items/10626/PlaybackInfo, the NJS VM is destroyed ===
2024/07/18 05:37:34 [warn] 20#20: *1178 js: redirect2Pan, UA: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0
2024/07/18 05:37:34 [warn] 20#20: *1178 js: hit cache routeL1Dict: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:34 [warn] 20#20: *1178 js: redirect to: https://ani.v300.eu.org/2024-7/[ANi] 魔王軍最強的魔術師是人類 - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2024/07/18 05:37:34 [warn] 20#20: *1178 js: === redirect2Pan: /emby/videos/10626/stream.mp4, the NJS VM is destroyed ===
抓包发现通过浏览器访问的Strm后设备接收到Nginx传回的Strm文件中的域名,但还是没办法正确播放
似乎是没有命中mediaMountPath的规则,所以默认启用了proxy模式,所以mediaMountPath应该填strm和local的父路径/media吗? 将mediaMountPath改为["/media"]后本地文件转码正常,但是浏览器无法播放Strm视频,ios客户端可以正常302 将mediaMountPath改为["/media/strm"]后浏览器还是不能正确播放Strm
1.抱歉代码中的判断优先级很混乱,以下仅为更精确的写法,鉴于当前已经正确判断了,所以不用更改,routeMode: proxy 和
routeMode: transcode 因历史遗留问题,这俩的效果是差不多的,仅是语义上的区别,所以 routeRule 中那条规则是无用的,我自己也搞混了
鉴于 /media 这个不是真正意义上的挂载工具多出来的根目录,下边混合得有 /media/strm 和 /media/local , 所以不建议使用简单的 mediaMountPath 参数了,需要换 mediaPathMapping 来处理,这样就不会覆盖判断了,以 routeRule 中的规则为准了
// 挂载工具 rclone/CD2 多出来的挂载目录, 例如将 od,gd 挂载到 /mnt 目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt
// 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 emby 处理,不用重复填写至 disableRedirectRule
// 如果没有挂载,全部使用 strm 文件,此项填[""],必须要是数组
const mediaMountPath = [""]; // 这里置空处理
// 路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍,不要有重叠
// 参数1: 0: 默认做字符串替换replace一次, 1: 前插, 2: 尾插, 3: replaceAll替换全部
// 参数2: 0: 默认只处理/开头的路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的
// 参数3: 来源, 参数4: 目标
const mediaPathMapping = [
// [0, 2, "/media/strm", ""], // 是我理解错了,这里不需要填这个
];
// 路由规则,注意有先后顺序,"proxy"规则优先级最高,其余依次,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内
// 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1
// "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链302, "transcode": 转码, "block": 只是屏蔽播放
// "transcode",稍微有些歧义,大部分情况等同于"proxy",这里只是不做转码参数修改,具体是否转码由 emby 客户端自己判断上报或客户端手动切换码率控制
// 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1
// 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接
// 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g)
// 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配)
const routeRule = [
["transcode", "LocalVideo", "filePath", 0, "/media/local"],
];
redirect to: https://xxx.eu.org/2024-7/[ANi] xxx - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2.strm 中的这个链接,有返回响应头 Access-Control-Allow-Origin: * 字段来表明允许跨域请求吗?web 浏览器和客户端对于跨域请求的默认处理是不一样的,客户端无视跨域请求,都可以访问,web 浏览器播放器没有做修改的话是禁止跨域请求的,可以在浏览器控制台中看到报错信息,假如是这个原因,可以参考,这个修改不局限于 115 链接,可解决所有跨域请求被拦截问题
好的,期待以后重构,我也觉得当前情况过于混乱 我觉得目前大致有三种情况:
前两者可以服务端传输流量,相对应的可以服务端进行转码等操作,对于网络URL建议直接返回链接让客户端重定向 这样可能Strm地址是内网Alist地址的用户可能就无法使用了,还有一个需求就是有先转码Strm再播放的(远程Strm文件经过客户端),但总的来说先大致分成这三个模块,对网络URL的模块后面可以再细分
似乎是没有命中mediaMountPath的规则,所以默认启用了proxy模式,所以mediaMountPath应该填strm和local的父路径/media吗? 将mediaMountPath改为["/media"]后本地文件转码正常,但是浏览器无法播放Strm视频,ios客户端可以正常302 将mediaMountPath改为["/media/strm"]后浏览器还是不能正确播放Strm
1.抱歉代码中的判断优先级很混乱,以下仅为更精确的写法,鉴于当前已经正确判断了,所以不用更改,routeMode: proxy 和 routeMode: transcode 因历史遗留问题,这俩的效果是差不多的,仅是语义上的区别,所以 routeRule 中那条规则是无用的,我自己也搞混了 ~鉴于 /media 这个不是真正意义上的挂载工具多出来的根目录,下边混合得有 /media/strm 和 /media/local , 所以不建议使用简单的 mediaMountPath 参数了,需要换 mediaPathMapping 来处理~,这样就不会覆盖判断了,以 routeRule 中的规则为准了
// 挂载工具 rclone/CD2 多出来的挂载目录, 例如将 od,gd 挂载到 /mnt 目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 emby 处理,不用重复填写至 disableRedirectRule // 如果没有挂载,全部使用 strm 文件,此项填[""],必须要是数组 const mediaMountPath = [""]; // 这里置空处理 // 路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍,不要有重叠 // 参数1: 0: 默认做字符串替换replace一次, 1: 前插, 2: 尾插, 3: replaceAll替换全部 // 参数2: 0: 默认只处理/开头的路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的 // 参数3: 来源, 参数4: 目标 const mediaPathMapping = [ // [0, 2, "/media/strm", ""], // 是我理解错了,这里不需要填这个 ]; // 路由规则,注意有先后顺序,"proxy"规则优先级最高,其余依次,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内 // 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1 // "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链302, "transcode": 转码, "block": 只是屏蔽播放 // "transcode",稍微有些歧义,大部分情况等同于"proxy",这里只是不做转码参数修改,具体是否转码由 emby 客户端自己判断上报或客户端手动切换码率控制 // 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 // 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接 // 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) // 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) const routeRule = [ ["transcode", "LocalVideo", "filePath", 0, "/media/local"], ];
redirect to: https://xxx.eu.org/2024-7/[ANi] xxx - 01 [1080P][Baha][WEB-DL][AAC AVC][CHT].mp4?d=true
2.strm 中的这个链接,有返回响应头 Access-Control-Allow-Origin: * 字段来表明允许跨域请求吗?web 浏览器和客户端对于跨域请求的默认处理是不一样的,客户端无视跨域请求,都可以访问,web 浏览器播放器没有做修改的话是禁止跨域请求的,可以在浏览器控制台中看到报错信息,假如是这个原因,可以参考,这个修改不局限于 115 链接,可解决所有跨域请求被拦截问题
也就是说只需要在routeRule处理,其他全部留空?
const routeRule = [
["transcode", "LocalVideo", "filePath", 0, "/media/local"], // 转码
["redirect", "AniOpan", "filePath", 0, "/media/strm"], // 重定向
];
我个人习惯是所有参数都显式,而不喜欢隐式,不然很容易造成混乱
是的,我也是这种倾向,这样写比较精确,可读性高
好的,期待以后重构,我也觉得当前情况过于混乱 我觉得目前大致有三种情况:
本地视频文件 本地软连接 网络URL
前两者可以服务端传输流量,相对应的可以服务端进行转码等操作,对于网络URL建议直接返回链接让客户端重定向 这样可能Strm地址是内网Alist地址的用户可能就无法使用了,还有一个需求就是有先转码Strm再播放的(远程Strm文件经过客户端),但总的来说先大致分成这三个模块,对网络URL的模块后面可以再细分
对于网络URL建议直接返回链接让客户端重定向
1.这个目前逻辑是这样的,属于自定义修改,但是需要说明下和源服务的差异,源服务/服务端 在 emby server 下原始行为是中转流量,而 jellyfin 对于 strm 这种特定文件是原始就支持重定向的,这和 emby 的行为不同,所以此脚本统一了这个行为
这样可能Strm地址是内网Alist地址的用户可能就无法使用了
2.这个已经有参数解决了此问题,但是属于脚本配置项中默认开启了,默认开启理由是我觉得这样更好,如果出现 bug 主要注释掉此条规则,参数名稍微有些描述上的歧义,是包括所以非 / 或 \ 开头的所有字符串
// 指定是否转发由 njs 获取 strm/远程链接 重定向后直链地址的规则,例如 strm/远程链接 内部为局域网 ip 或链接需要验证
// 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1
// 参数2: 匹配类型或来源(字符串参数类型),默认为 "filePath": mediaPathMapping 映射后的 strm/远程链接 内部链接
// ,有分组时不可省略填写,可为表达式
// 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g)
// 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配)
const redirectStrmLastLinkRule = [
[0, strHead.lanIp.map(s => "http://" + s)],
];
还有一个需求就是有先转码Strm再播放的(远程Strm文件经过客户端)
3.这个行为在 emby server 原始是默认支持的,但是个人考虑到体验感受上,脚本默认禁用了 strm 的转码功能,这个决定是基于我 NAS 本身性能太差,相当于无显卡,分片地址在客户端上如果超过 10 秒没有响应,客户端会主动断开请求并重新请求,会导致对同一段切片重复转码,重复浪费硬件性能和网络带宽,所以只在转码硬件 + 上传带宽同时良好的情况下才有好的体验,故默认禁用了,也可打开 enableStrmTranscode: true , 调试一下,这个选项的前提需要去掉 routeRule 中 strm 的那条 redirect 规则,达到的效果为在客户端上报要转码 + 手动切换至比原视频更小的码率时走转码,其余会默认走直链,
括号内的补充暂未理解意思
const transcodeConfig = {
enable: true, // 此大多数情况下为允许转码的总开关
enableStrmTranscode: true, // 默认禁用 strm 的转码,体验很差,仅供调试使用
redirectTransOptEnable: true, // 是否保留码率选择,不保留官方客户端将无法手动切换至转码
};
括号内的意思其实就是想说如果客户端没有请求转码,服务端就让客户端重定向到strm文件中的url,如果客户端请求转码,服务端可以转码后在发给客户端
其实jellyfin对strm文件的处理并不是完全直接重定向 我之前测试过,只要服务端开启转码功能,即便客户端没有请求转码,服务端也会代理,还有就是客户端访问失败服务端也会代理
好的,多谢补充留个档,jellyfin 我自身测试得少,因为和新 emby 历史入库数据无法通用,建测试库太疼苦了,只要一扫库,整个 web 页面响应都卡死了,所以平时都只用 emby server
刚刚发现,如果strm文件是内网的alist url下载地址,nginx不会去访问alist获取raw_url,这里需要如何配合
1.脚本中对于已经是远程连接的路径,即非 / 或 \ 开头的,默认都是直接响应了 302 到链接上,因为 strm 的规范特性决定了都是直接链接,所以没有获取 alist 的 raw_url (仅限这个字段,是 alist api 接口中的)
2.不过对于内网开头的 strm 内部链接,确实默认规则里写了条获取重定向后的地址进行响应的参数,位于 emby2Alist\nginx\conf.d\config\constant-strm.js,可以注释这条 [0, strHead.lanIp.map(s => "http://" + s)], 这样就和 1 中的行为一致了
// 指定是否转发由 njs 获取 strm/远程链接 重定向后直链地址的规则,例如 strm/远程链接 内部为局域网 ip 或链接需要验证
// 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1
// 参数2: 匹配类型或来源(字符串参数类型),默认为 "filePath": mediaPathMapping 映射后的 strm/远程链接 内部链接
// ,有分组时不可省略填写,可为表达式
// 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g)
// 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配)
const redirectStrmLastLinkRule = [
[0, strHead.lanIp.map(s => "http://" + s)],
// [0, alistAddr],
// [0, "http:"],
// 参数5: 请求验证类型,当前 alistAddr 不需要此参数
// 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime
// [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`],
// useGroup01 同时满足才命中
// ["useGroup01", "filePath", "startsWith", strHead.lanIp.map(s => "http://" + s)], // 目标地址
// ["useGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型
// docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr)
// ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网
];
emby2Alist\nginx\conf.d\config\constant-common.js
// 字符串头,用于特殊匹配判断
const strHead = {
lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头
xEmbyClients: {
seekBug: ["Emby for iOS", "Infuse"],
maybeProxy: ["Emby Web", "Emby for iOS", "Infuse"],
},
"115": "115.com",
ali: "aliyundrive.net",
};
emby2Alist\nginx\conf.d\config\constant-strm.js,可以注释这条 [0, strHead.lanIp.map(s => "http://" + s)], 这样就和 1 中的行为一致了
注释掉这条配置后对于公网可以直接访问的strm有什么影响吗
没有任何影响,当初默认加上这条仅仅只是看到 issus 中有人的 strm 内部链接写成了内网地址,导致响应 302 内网地址到达客户端时,客户端设备不在内网的情况下无法播放,所以写了这个处理,方法名稍微有些歧义,实际仅获取了一次重定向后地址,内网链接后的响应取 Location 头然后响应给客户端了
没有任何影响,当初默认加上这条仅仅只是看到 issus 中有人的 strm 内部链接写成了内网地址,导致响应 302 内网地址到达客户端时,客户端设备不在内网的情况下无法播放,所以写了这个处理,方法名稍微有些歧义,实际仅获取了一次重定向后地址,内网链接后的响应取 Location 头然后响应给客户端了
刚刚测试了一下,经抓包发现传递给前端的依旧是内网的URL
我的strm里面是一个alist的直连,链接的域名是alist.com
,我启动容器时添加了--add-hosts
将该域名指向了172.22.0.2
[0, strHead.lanIp.map(s => "http://" + s)],
,重启Nginx后,发现传递到客户端的依旧是是alist.com
lanIp: ["172.", "10.", "192.", "[fd00:", "alist.com"]
,重启Nginx后,发现传递到客户端的依旧是是alist.com
1.nginx 的 NJS 中对于域名默认确实是不解析的,只有 ngx.fetch 和 r.subrequest 中才会用到 DNS 做域名解析且此过程不可见无法干预,其余情况都是当作文本字符串处理的,所以就有了测试中的情况
刚刚测试了一下,经抓包发现传递给前端的依旧是内网的URL,我的strm里面是一个alist的直连,链接的域名是alist.com
2.这个是否是 emby 入库的时候媒体路径已经被更改为了内网地址?这个可以在媒体详情页底部查看, 理论上注释这条 [0, strHead.lanIp.map(s => "http://" + s)], 后,就是直接响应的 emby 入库时从 strm 中读取的地址,即媒体路径(strm 这里是一个远程链接地址,emby 元信息统一是用媒体路径字段),进行 nginx 的劫持响应了
启动容器时添加了--add-hosts将该域名指向了172.22.0.2
所以这个操作对于 NJS 的脚本来说是忽略的
这个是否是 emby 入库的时候媒体路径已经被更改为了内网地址?这个可以在媒体详情页底部查看
我查看了一下之前增加公网可以直接访问的strm视频下方确实有对应的url,但此次增加的strm下方并没有,我还重新添加了媒体库确实没有看到链接,不过我重新添加至媒体库后给服务端推送的依旧是alist.com
alist.com
1.这个其实是符合预期的结果,直接响应 strm 内部的文本链接
第二次测试:我怀疑是nginx并没有解析域名,直接向客户端返回url了,所以我将修改了lanIp: ["172.", "10.", "192.", "[fd00:", "alist.com"] ,重启Nginx后,发现传递到客户端的依旧是是alist.com
2.这个好像是不符合预期结果,如果不好排查的话,可以手动写完整的字符串来可读性高一些
const redirectStrmLastLinkRule = [
[0, ["http://alist.com"]],
];
2.1 需要注意下是 http 还是 https 开头的 2.2 规则的行为 0: startsWith(str) 是检测到 emby item 的 path 字段为指定字符串开头的时候,脚本先访问一次 path 字段的链接,获取该链接返回的 Location 响应头中的待跳转链接后再由脚本响应新的链接地址,所以还有种可能是 strm 内部文本链接的 http://alist.com/xxx 这个链接返回的 Location 响应头依旧是 http://alist.com/xxx 形式开头的,这个结果符合代码逻辑,但不太符合设计初衷,设计初衷是给 strm 内部链接误写为内网地址了,但是批量改起来比较麻烦,所以加了可以指定 http://172. 开头类似的内网地址,获取一次 Location 响应头按预期应该得到一个公网可访问的地址,响应给客户端设备访问
配置了
const redirectStrmLastLinkRule = [
[0, ["http://alist.com"]],
];
下面配置就修改回默认状态吗
const strHead = {
lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头
xEmbyClients: {
seekBug: ["Emby for iOS", "Infuse"],
maybeProxy: ["Emby Web", "Emby for iOS", "Infuse"],
},
"115": "115.com",
ali: "aliyundrive.net",
};
const redirectStrmLastLinkRule = [
[0, strHead.lanIp.map(s => "http://" + s)],
// [0, alistAddr],
// [0, "http:"],
// 参数5: 请求验证类型,当前 alistAddr 不需要此参数
// 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime
// [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`],
// useGroup01 同时满足才命中
// ["useGroup01", "filePath", "startsWith", strHead.lanIp.map(s => "http://" + s)], // 目标地址
// ["useGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型
// docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr)
// ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网
];
嗯,strHead 这个最好还是默认,测试时可以修改,redirectStrmLastLinkRule 这个是同一个,可以合并下
const strHead = {
lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头
xEmbyClients: {
seekBug: ["Emby for iOS", "Infuse"],
maybeProxy: ["Emby Web", "Emby for iOS", "Infuse"],
},
"115": "115.com",
ali: "aliyundrive.net",
};
const redirectStrmLastLinkRule = [
[0, ["http://alist.com"]],
// [0, strHead.lanIp.map(s => "http://" + s)],
// [0, alistAddr],
// [0, "http:"],
// 参数5: 请求验证类型,当前 alistAddr 不需要此参数
// 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime
// [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`],
// useGroup01 同时满足才命中
// ["useGroup01", "filePath", "startsWith", strHead.lanIp.map(s => "http://" + s)], // 目标地址
// ["useGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型
// docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr)
// ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网
];
我研究了一下Emby的API,客户端通过emby/Videos/{itemID}/stream.{Container}
这个API获取视频数据,但我没搞懂emby2Alist是如何处理Strm文件的,望指教
我通过/Shows/{Id}/Episodes
查看某个剧集季度的详情,得到如下结果
{
"Items": [
{
"Name": "羊的玩偶服",
"ServerId": "3bb1f8e82a5c45f0b72aefb47f7f18a8",
"Id": "16276",
"PremiereDate": "2024-07-07T00:00:00.0000000Z",
"RunTimeTicks": 13830960000,
"IndexNumber": 1,
"ParentIndexNumber": 1,
"IsFolder": false,
"Type": "Episode",
"ParentLogoItemId": "16269",
"ParentBackdropItemId": "16269",
"ParentBackdropImageTags": [
"bab04314e8e755cc7a4b959c7576e3bf",
"21d8cfe44daa6fb2befd3ee0444d8307"
],
"SeriesName": "小市民系列",
"SeriesId": "16269",
"SeasonId": "16275",
"SeriesPrimaryImageTag": "a1785bdeb3a8ee3f9d1f5bd6f80324c6",
"SeasonName": "季 1",
"ImageTags": {
"Primary": "baf79d02ed55af7194af0770d1f00a47"
},
"BackdropImageTags": [],
"ParentLogoImageTag": "301e87d9387f92117186c66e65be50db",
"MediaType": "Video"
},
{
"Name": "羊的玩偶服",
"ServerId": "3bb1f8e82a5c45f0b72aefb47f7f18a8",
"Id": "20023",
"PremiereDate": "2024-07-07T00:00:00.0000000Z",
"IndexNumber": 1,
"ParentIndexNumber": 1,
"IsFolder": false,
"Type": "Episode",
"ParentLogoItemId": "20016",
"ParentBackdropItemId": "20016",
"ParentBackdropImageTags": [
"d956b321e857376d27a98777d963b680",
"342880eef02a29b18ba8690e12709260"
],
"SeriesName": "小市民系列",
"SeriesId": "20016",
"SeasonId": "20022",
"SeriesPrimaryImageTag": "ddba46853859b591cb9a2c3ab982626d",
"SeasonName": "季 1",
"ImageTags": {
"Primary": "89d86d22e39254f69e7c2b8dd4b575f4"
},
"BackdropImageTags": [],
"ParentLogoImageTag": "c4215b2e3de1832aea9823bfa3e30b29",
"MediaType": "Video"
}
],
"TotalRecordCount": 4
}
这是同一个电视剧同一季度同一集,区别在于一个是MKV文件,另一个是Strm文件,两个文件存放的路径并不相同,我发现他们的ID并不相同,分别是16276
和20023
接着我在服务端分别播放这两个视频,查询Nginx的日志(省略查询参数)
GET /emby/Videos/16276/stream.mkv
GET /emby/Videos/16276/stream.strm
发现两种均在请求16276
,只是Container不同,但是strm那条确实是服务器并未上传流量,我想知道emby2Alist是如何利用Emby的API对达到302重定向的效果的,请指教
查询Nginx的日志(省略查询参数)
GET /emby/Videos/16276/stream.mkv GET /emby/Videos/16276/stream.strm
我发现前者的http code是206,后者是302,但emby2Alist具体实现方法还是不了解
Emby的API,客户端通过emby/Videos/{itemID}/stream.{Container}这个API获取视频数据
1.确实是这个加载视频的二进制流数据,但是 /stream.{Container} 这一级路径在官方的说明上稍微有些误导,这个 API 其实只用匹配到 emby/Videos/{itemID} 前面这段就行了,/stream.{Container} 可以使用任意名称,例如 /中文影片名.任意后缀名 也是可以的
ID并不相同,分别是16276和20023
2.ItemId 不同是正确的,emby 中任意一个对象都有单独的 id 标识,两个不同的文件也是不同的 id
3.strm 的判断方式的话,这边为了囊括更多情况,所以可能条件写得有些冗余,具体是在 emby.js 的 487 ,即 fetchEmbyFilePath 中,以下为条件片段,
3.1 mediaSource.IsInfiniteStream 这个 emby ItemInfo API 中是直播流(无限流) 3.2 mediaSource.IsRemote 这个只是初步说明是远程链接媒体,不是绝对的,直播流,STRM 内部为远程链接的在首次播放前为 true,播放一次后刮削补充入库,此时 IsRemote 本身的值 emby 自身都 bug 了,会变为 false, 所以就有了 3.3 的补充修复判断方式 3.3 util.checkIsStrmByPath(item.Path); 这个是为了兼容此脚本强行支持的 strm 内部为 / 开头的自定义相对文件路径风格的补充判断,需要注意这个取的是 item 对象的 path,是一个 strm 后缀文件所在路径地址,所以名称中包含 .strm 后缀的字符串,而非 MediaSourceInfo 对象的 path,这两个对象的 Path 是不同的,在 strm 文件首次播放后 MediaSourceInfo 的 path 会变为一个刮削后文件路径
/**
* note1: MediaSourceInfo{ Protocol }, String ($enum)(File, Http, Rtmp, Rtsp, Udp, Rtp, Ftp, Mms)
* note2: live stream "IsInfiniteStream": true
* eg1: MediaSourceInfo{ IsRemote }: true
* eg1: MediaSourceInfo{ IsRemote }: false, but MediaSourceInfo{ Protocol }: File, this is scraped
*/
rvt.notLocal = mediaSource.IsInfiniteStream
|| mediaSource.IsRemote
|| util.checkIsStrmByPath(item.Path);
3.4 总结起来是 emby API 中已经入库的信息 + 特殊情况的 .strm 文件后缀,共同判断是远程媒体链接/路径,当然有个例外,即导致 emby,js 第 120 行,isRemote 为 true,util.isAbsolutePath 这个是为了兼容 mediaPathMapping 路径映射参数,所以最后重新简单判断了一次,以兼容路径映射中用户自定义把 strm 中一些远程链接前缀移除掉了,此时就视作本地文件处理,去走下边之前的 alist 查询文件路径这块,虽然估计一般没人这么干,只是防止特殊情况加了这个逻辑而已
isRemote = !util.isAbsolutePath(mediaItemPath);
GET /emby/Videos/16276/stream.mkv GET /emby/Videos/16276/stream.strm 前者的 http code 是206,后者是302
4.这个具体看 /Items 这个 API ,当然和 /emby/Users/ac0d220d548f43bbb73cf9b44b2ddf0e/Items/415928 这个 UserIremsInfo API 返回的内容结构是一样的,进入详情页就会调用, 前面这个的大概率是 path 判断出来非挂载路径强制走回源中转的路由模式了,日志和代码中关键字 hit proxy, not mountPath first, 后者是 3 中的判断命中了默认强制直接 302 响应 strm 内部远程链接的规则, util 的 getItemInfo ,
itemInfoUri = `${embyHost}/Items?Ids=${newMediaSourceId ?? mediaSourceId}&Fields=Path,MediaSources&Limit=1&api_key=${api_key}`;
我写了一个Go的反向代理服务器, 一般情况下所有请求会直接转发给Emby进行处理,但是检测到是Strm后会响应一个302的重定向 为了方便测试,我直接返回了一个固定的链接,然后我在客户端进行测试,发现客户端确实在请求对应域名,但是客户端无法正常播放,是需要额外处理吗
1.emby/jellyfin 官方客户端能否直接播放取决于两个接口
1.1 location ~ /Items/(.)/PlaybackInfo 这个 API 是播放的同时,更精确的话是播放串流 API 之前,客户端会上报自己的解码情况和码率选项,不过入参不用管,我这边也没做任何使用,除了转码相关的最高码率参数外,然后服务端会返回,是否支持直接串流 > 是否支持转码,等布尔值标记,PlaybackInfo 仅是信息查询,不能返回 302 状态码
1.1.1 为保证直接播放(串流),反代根据路由规则强制修改了两个标志位为 true,source.SupportsTranscoding 这个标志位可以不用管,也可保险点儿修改为 false,因为 emby 客户端在未选择低码率的情况下,默认都是直接串流播放,选择的码率会被客户端记住,播放其它所有视频都会作为入参上报,满足转码上报情况的,串流那两个标志位会被返回为 false, SupportsTranscoding 此时为 true, emby.js > transferPlaybackInfo > // 防止客户端转码(转容器)modifyDirecPlaySupports(source, true);
function modifyDirecPlaySupports(source, flag) {
source.SupportsDirectPlay = flag;
source.SupportsDirectStream = flag;
...
}
1.1.2 需要自行拼接 DirectStreamUrl 字段,因为假如源服务 SupportsDirectStream 返回 false,此字段会消失,不过需要注意 source.DirectStreamUrl 串流地址和 source.TranscodingUrl 转码地址,两个都是不带协议域名端口的相对地址,/emby/Videos/16276/stream.strm 和 /Videos/16276/stream.strm 都可以,/emby 这一级不重要,emby 官方文档和 API 中都是互相混用的, emby.js > transferPlaybackInfo > modifyDirecPlayInfo(r, source, body.PlaySessionId);
source.DirectStreamUrl = encodeURI(source.DirectStreamUrl);
1.2 /emby/Videos/16276/stream.strm 这个直接串流的 API 地址,在 emby 中直接取的就是 PlaybackInfo 接口返回的 DirectStreamUrl 字段拼接上前端 app 的前缀网址得出的,jellyfin 中稍微有些不同,会忽略 DirectStreamUrl ,全部由前端拼接完成,但是两者都会根据 SupportsDirectStream 为 true 来决定走串流播放,所以真正需要劫持返回 302 的是此 API 地址
21.根据截图代码变量命名猜测,可能是 302 错了 API,修改到 PlaybackInfo 上了,需要换成 /emby/Videos/16276/stream.strm 这种地址的返回 302 状态和链接
2.2 当然根据日志打印感觉又没错,那问题可能出现在 PlaybackInfo 这个播放的前置请求上,大概率上游源服务返回了 SupportsDirectStream 为 false,导致没有了 DirectStreamUrl 地址
在客户端进行测试,发现客户端确实在请求对应域名,但是客户端无法正常播放
2.3 但是这点推测 2.1 和 2.2 是不存在的,暂时能想到的可能只有上游服务返回了 source.SupportsDirectPlay = false; source.SupportsDirectStream = true; 这种情况,这个需要抓包看下了,假如不是此问题,就只能换官方魔改版那种试试了,API 流程上和官方客户端一致,但是播放器方面增强为 libmpv 之类的本地播放器了,以此排除客户端 web 播放器方式不兼容的媒体格式这种可能
2.2 当然根据日志打印感觉又没错,那问题可能出现在 PlaybackInfo 这个播放的前置请求上,大概率上游源服务返回了 SupportsDirectStream 为 false,导致没有了 DirectStreamUrl 地址
你的猜测很正确,我使用的是emby+第三方客户端,我这里是修改/emby/Videos/16276/stream.strm
的返回结果,我只路由匹配/emby/Videos/16276/stream.strm
这种规则,原来还需要匹配/Items/(.*)/PlaybackInfo
,那我再修改试试
所以播放的逻辑是:
/Shows/{Id}/Seasons
和/Shows/{Id}/Episodes
获得季详情和集详情,/Items/{Id}/PlaybackInfo
获取MediaSources,MediaSources是一个列表,每一个MediaSources都是该集的一个版本(1080p、4K等版本选择)/Videos/{Id}/stream.{Container}
和MediaSource播放视频不知道我的理解有没有错误,感觉整个API最混乱的就是ID这个字样。。。要被绕晕了
可能是我Strm文件本身内部就是直链,所以mediasource.Protocol是http,所以mediasource.Path本身并不是文件的在EmbyServer容器内的路径,而是Strm文件内容,也就是视频直链;mediasource.SupportsDirectPlay和mediasource.SupportsDirectStream也是true,mediasource.SupportsTranscoding也是true,但我还是拦截了/Items/:id/PlaybackInfo
的请求,确保响应一致
/Items/:id/PlaybackInfo
的请求),App提示处理请求发生错误/Items/:id/PlaybackInfo
的,但我的函数(上图代码24行)emby.PlayBackInfo
是使用Get请求原始地址的,简单对比后,发现Get和Post请求同一API响应内容相似,并未额外写一个参数,沿用之前写的函数,即emby.PlayBackInfo
,并且简单处理后(第30~36行)返回结果/Items/:id/PlaybackInfo
的请求)浏览器可以正常访问,抓包发现响应结果正确,mediasource.SupportsTranscoding也被确保为false/videos/:id/:name
接口的处理
1.播放的逻辑顺序理解没啥问题,确实 emby 内部 ID 的标识很乱
为了保证可行性,我使用IOS端的Emby官方APP进行测试,结果在打开电视剧每一集的详情(也就是发起/Items/:id/PlaybackInfo的请求),App提示处理请求发生错误
2.这个有些似曾相识,不过不一定是一样的,具体是自定义实现反代后,NJS 中是通过 r.subrequest() 发送了达到上游服务的新请求,并且接受到了响应,但是如果原请求没有返回的响应体 Content-Type 不同的话客户端会报错,看到代码中 ctx.JSON() 这个应该已经避免了这个问题,不过原请求的其它响应头还必须从上游接口响应头中复制一遍,因为原请求所对应的响应对象中头部是空的,emby.js > transferPlaybackInfo > util.copyHeaders(response.headersOut, r.headersOut); 不过需要注意 Content-Length 响应头得删除掉,让客户端自身进行自动计算,不然也会导致客户端报错
if (!skipKeys) {
// auto generate content length
skipKeys = ["Content-Length"];
}
3.之前有个小区别忘了提了,发起 /Items/:id/PlaybackInfo 的请求,这个在 emby 中总共会有两次,一次是在进入详情页的同时调用的,链接入参的 IsPlayback === "false"; , 第二次才是上述一直提到的点击播放按钮的同时调用的,此时 IsPlayback 为 true, jellyfin 的话 PlaybackInfo 调用时机只有第一次不同,在进入详情页的时候,没有使用这个 API,而是使用的更加通用的 ItemInfo 的 API 接口查询的媒体详情数据
感谢老哥指点迷津,现我已在iOS官方Emby APP上实现了302播放,但是对于一些非官方App(iOS上的 Conflux)还是播放失败(使用emby2Alist则正常)不知道老哥有没有思路,以下是一些差异
(额外说一句,Emby的API真的迷惑,居然Path、Query的Key对大小写不敏感,但是Query的value对大小写敏感,有些APP是大驼峰,有些APP是小驼峰)
1.1 PlaybackInfo 这个接口理解稍微有些偏差,请求时机的确是都只会在播放的时候请求这个API,MediaSources 是一个数组,已经包含了该集的全部版本,补充下 UserItems 接口和通用的 Items 接口的返回内容也会包含 MediaSources 这个多版本的数组
1.2 GET 和 POST 的区别我猜测是官方客户端上报设备的兼容解码信息和已选择的码率参数所以使用 POST,而第三方客户端不用上报这些信息,基本都是取直接串流的字段链接播放,所以用了 GET
2.1 这个差异是正常的,应该没太大问题
2.2 😂我之前也被这命名风格搞混乱了,不过应该是包含了两部分时期的变量风格,之前兼容 jellyfin 的时候发现的,emby 未分家前,变量基本都是小写驼峰开头,新版 emby 都是大写驼峰的,为了兼容老客户端吧,就都混在一起了
3.暂时没有明确的问题思路,只能猜几个了
3.1 对比了下 PlaybackInfo 的 GET 和 POST ,在 POST 不传请求体的时候,返回内容是一样的,应该和请求方式无关,替换内容报错感觉可能和压缩有关,可以尝试覆盖客户端请求头中协商的压缩编码信息,proxy_set_header Accept-Encoding ""; ,客户端一般会上报为 Accept-Encoding: gzip, deflate, br ,当然只是可能与这个有关,日志中能打印出响应体中不乱码的中文的话就不不是此问题了
3.2 串流地址需要匹配 original 这个关键字,location ~ /videos/(.)/(stream|original) ,新客户端确实都是 /emby/Videos/354/stream.mp4 ,但部分老客户端会使用 /emby/Videos/354/original.mp4 这样的串流地址,当然从 2 的日志中看到是 stream ,所以不太确实这个影响
今天又测试了一下,发现Conflux
请求了/Items/26/PlaybackInfo
,官方API文档也是,感觉有些需要加上prefix/emby
而有些又不用
prefix/emby
这个确实很乱,不过我这边没对比过,只是粗略的认为 /emby
这个类似于 web context 这种,但是有和没有这级 path 在 emby 的 API 上都是兼容的,它内部应该做了处理,当然实际情况是否如此我这边没测试过,所以看到 conf 的 location 中之前作者都是用的模糊匹配,最好不要带上/emby
这级路径的精确匹配了
请问这个是正常情况吗,是不是以后只能通过客户端访问了