-
Notifications
You must be signed in to change notification settings - Fork 41
Open
Labels
bugSomething isn't workingSomething isn't workinghelp wantedExtra attention is neededExtra attention is needed
Description
摘要
当前应用与 Jellyfin/Emby 服务器的播放交互采用了极其简化的URL拼接方式,完全绕过了官方核心的 POST /Items/{id}/PlaybackInfo 播放协商协议。这一根本性缺陷导致了一系列严重问题:
- 转码失败:首次转码正常,但后续切换清晰度必定失败。
- 资源泄露:播放停止后,服务器端的
ffmpeg转码进程会持续运行直至超时,无法被立即释放。 - 功能受限:不支持多音轨/字幕选择、直播流、从指定时间点播放等高级功能。
- 代码冗余:
JellyfinService与EmbyService中存在大量重复的播放逻辑,维护成本高。
本次重构旨在废弃现有的URL拼接方案,完整实现官方的 PlaybackInfo 协议,建立统一、健壮的播放会话管理机制,从根源上解决上述所有问题。
当前行为与问题分析
-
播放启动流程:
- 入口: 用户在
MediaServerDetailPage点击媒体项。 - 流程: 调用
JellyfinService.getStreamUrl()或EmbyService.getStreamUrl()。 - 缺陷: 这两个函数直接拼接出
.../Videos/{itemId}/stream或.../master.m3u8格式的URL,仅将本地设置中的码率作为查询参数附加。全程没有向服务器发送POST /Items/{id}/PlaybackInfo请求来声明设备能力(DeviceProfile)并获取播放许可。
- 入口: 用户在
-
清晰度切换失败:
- 入口: 播放器内切换清晰度。
- 流程: 调用
VideoPlayerState.reloadCurrentJellyfinStream(),该函数再次调用JellyfinService.buildHlsUrlWithOptions()重新拼接一个新的 HLS URL。 - 缺陷: 服务器将这次请求视为一个全新的、无上下文的播放请求,无法关联到已存在的转码会话。因此,客户端无法获取到正确的 HLS 切片信息,导致切换失败。
-
FFmpeg 进程悬挂:
- 入口: 用户停止播放。
- 流程:
VideoPlayerState调用JellyfinPlaybackSyncService.reportPlaybackStopped()。 - 缺陷: 该服务上报给
/Sessions/Playing/Stopped接口的PlaySessionId是由客户端通过_generatePlaySessionId()伪造的 (nipaplay_{timestamp}_{itemId})。服务器不认识这个ID,自然无法找到并终止与之关联的ffmpeg进程,导致其成为“僵尸进程”直至超时。
期望行为
-
规范的播放握手:
- 在播放开始前,客户端应构建包含设备能力的
PlaybackInfoRequest(含DeviceProfile),并向POST /Items/{id}/PlaybackInfo发起请求。 - 客户端解析服务器返回的响应,获取其中包含的真实的
PlaySessionId、可用的媒体源(MediaSources)、以及服务器生成的直播/转码URL(TranscodingUrl)。
- 在播放开始前,客户端应构建包含设备能力的
-
基于会话的交互:
- 所有后续播放操作(如进度上报、切换音轨/字幕)都必须携带服务器返回的真实
PlaySessionId。 - 切换清晰度时,应使用同一个
PlaySessionId重新请求 PlaybackInfo 或直接使用其提供的 HLS 主播放列表,确保会话连续性。
- 所有后续播放操作(如进度上报、切换音轨/字幕)都必须携带服务器返回的真实
-
即时的资源释放:
- 播放停止时,向
/Sessions/Playing/Stopped接口发送包含真实PlaySessionId的请求,使服务器能够立即定位并终止对应的转码进程。
- 播放停止时,向
-
统一的抽象:
- Jellyfin 和 Emby 的播放逻辑应被抽象到统一的客户端或服务中,共享大部分协议实现,仅在路径前缀、参数细节等处作区分。
建议的重构计划
为了系统性地解决此问题,建议分阶段实施以下重构计划:
-
1. 搭建抽象层与数据模型
- 在
services目录下创建统一的MediaServerPlaybackClient抽象接口。 - 定义核心的
PlaybackSession数据模型,用于承载从服务器获取的itemId,mediaSourceId,playSessionId,streamUrl,transcodingInfo等关键信息。 - 定义
DeviceProfile及相关模型,用于向服务器描述客户端的播放能力(支持的容器、编解码器、字幕格式等)。
- 在
-
2. 实现 PlaybackInfo 核心流程
- 在
MediaServerPlaybackClient的 Jellyfin/Emby 实现类中,完成POST /Items/{id}/PlaybackInfo的请求与响应逻辑。 - 能够根据当前平台(桌面/移动/Web)和播放器设置,动态生成
DeviceProfile。 - 将服务器响应成功解析并填充到
PlaybackSession对象中。
- 在
-
3. 改造播放器状态管理 (
VideoPlayerState)- 修改
initializePlayer方法,使其不再接收写死的actualPlayUrl,而是接收一个PlaybackSession对象。 - 播放器启动时,使用
PlaybackSession中的streamUrl进行播放。 - 重构清晰度、音轨、字幕的切换逻辑,使其通过更新或重新获取
PlaybackSession来实现,确保PlaySessionId的复用。
- 修改
-
4. 更新播放同步服务
- 改造
JellyfinPlaybackSyncService和EmbyPlaybackSyncService。 - 在
reportPlaybackStart/Progress/Stopped等方法中,使用PlaybackSession中真实的PlaySessionId。 - 移除客户端本地伪造
PlaySessionId的_generatePlaySessionId方法。
- 改造
-
5. 功能扩展与兼容
- 基于
PlaybackInfo响应中的MediaSources列表,实现对多媒体源(如不同版本文件)、直播流的支持。 - 添加对
StartPositionTicks,AudioStreamIndex,SubtitleStreamIndex等参数的支持,允许从指定位置播放和预选音轨/字幕。
- 基于
-
6. 清理与收尾
- 在所有播放入口(如
MediaServerDetailPage,PlaylistMenu等)切换到新的MediaServerPlaybackClient。 - 确认所有旧的
getStreamUrl*和buildHlsUrl*方法均无引用后,予以安全删除。
- 在所有播放入口(如
受影响的文件
lib/services/jellyfin_service.dartlib/services/emby_service.dartlib/utils/video_player_state.dartlib/services/jellyfin_playback_sync_service.dartlib/services/emby_playback_sync_service.dartlib/pages/media_server_detail_page.dartlib/services/playback_service.dart- 以及其他所有发起播放请求的UI组件。
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workinghelp wantedExtra attention is neededExtra attention is needed