Instruction file imported from wind007/ma_palyer (
.cursor/rules/session-2026-04-20-player-worklog-zh.mdc). Copyright stays with the author.
今日工作记录(播放器)
目标与背景
- 项目为 Flutter + Emby + fvp 的多端播放器。
- 今天重点处理:启动报错、版本信息显示、详情页版本交互、播放器内版本切换、音轨字幕选择逻辑。
今日已完成改动
- 修复主题类型兼容:
CardTheme->CardThemeData(lib/main.dart)。 - 依赖兼容回退:
video_player固定为2.9.5,避免与当前fvp组合冲突(pubspec.yaml)。 - 新增亮度控制:播放页采用播放器亮度能力并补充画面叠层兜底,保证亮度手势在非 TV 平台有可见效果(
lib/pages/video_player_page.dart)。 - 桌面端转场优化:Windows/Linux/macOS 使用更轻的页面过渡动画(
lib/main.dart)。 - 播放器顶部版本信息:显示完整格式(如
1080p · H264 · AAC 5.1)。 - 详情页大图版本信息:右上角显示“当前版本”标签,并与版本选择联动。
- 详情页版本卡高亮:当前版本卡片高亮并标记“当前”。
- 修复详情页“选择音轨/字幕”弹窗逻辑:改为“先选后确认播放”,不再点一下就关闭弹窗。
- 优化详情页交互:当音轨<=1 且字幕<=1 时,点击版本直接播放,不弹选择框。
- 实现播放器内版本切换:新增版本选择入口与切换逻辑,切换后保留进度/音量/倍速/播放状态。
- 修复版本切换后音轨字幕上下文错位:音轨/字幕切换统一使用当前版本索引。
- 修复版本标签总是 720p 的问题:版本分辨率优先从
MediaSource.Name/Path解析,其次MediaSource.Width/Height,最后才回退流参数。
关键结论(避免重复踩坑)
flutter run -t lib/pages/video_player_page.dart不是合法启动方式(页面文件不是入口),会触发 isolate 启动失败。- 必须从
lib/main.dart启动:flutter run -d windows或flutter run -d windows -t lib/main.dart。 fvp升级时常受mdk-sdk下载源影响;网络异常会导致构建失败,需优先确认依赖下载完整性。- 版本信息显示不能仅依赖播放流参数,否则容易被转码链路误导(例如固定显示 720p)。
后续建议
- 继续验证“版本切换是否真实生效”时,优先观察 Emby 会话中的
MediaSourceId是否变化。 - 若需要更强可见性,可增加切换成功提示(含版本标签与
MediaSourceId)。 - 可将详情页与播放页的版本标签解析逻辑抽到公共工具,避免双处维护。
参考(本次问题排查优先来源)
- Emby API 文档:
https://dev.emby.media/reference/RestAPI.html - Emby API 说明页:
https://github.com/MediaBrowser/Emby/wiki/Api - fvp 包页面:
https://pub.dev/packages/fvp - fvp 源码:
https://github.com/wang-bin/fvp - mdk-sdk wiki:
https://github.com/wang-bin/mdk-sdk/wiki
追加记录(本轮新增改动)
- 播放页新增“切换内容”入口:剧集/系列在顶部控制栏可直接打开/关闭列表面板(
lib/pages/video_player_page.dart)。 - 切换列表展示状态增强:每集显示“已看完成”图标或观看进度条(基于
UserData.Played与PlayedPercentage)。 - 列表默认选中当前集:打开列表时自动聚焦当前播放内容,避免用户手动查找。
- 长列表滚动稳定性优化:使用统一
itemExtent+ 偏移量边界保护,集数很多时仍可准确定位与滚动。 - 首页“继续观看”补全剧集类型:
Resume请求显式加入IncludeItemTypes=Movie,Episode,Series,并同步到分页加载(lib/services/emby_api.dart、lib/pages/video_list_page.dart)。
追加验证建议
- 首页下拉刷新后确认“继续观看”出现电视剧剧集(Episode)。
- 播放页打开列表时确认:默认焦点在当前集,且滚动位置正确。
- 集数较多(>50)时,连续上下移动焦点,确认列表滚动不中断、不跳错位。
追加记录(切换内存安全)
- 目标:降低“切换字幕/版本/音轨”高频操作时的内存泄漏与状态污染风险(
lib/pages/video_player_page.dart)。 - 新增并发互斥:
_isControllerSwitching,切换进行中忽略重复切换请求。 - 新增会话令牌:
_switchSession,每次切换生成新会话;异步返回若会话已过期则中止并释放新建控制器,避免旧异步结果覆盖新状态。 - 在
dispose()中递增_switchSession,确保页面销毁后未完成异步自动失效。 - 在会重建播放器控制器的路径中应用上述保护:
_switchMediaSource(...)、_switchAudioStream(...)。 - 保持控制器生命周期闭环:旧控制器统一移除监听并
dispose,新控制器再注册监听,避免监听器残留。
追加验证建议(内存与稳定性)
- 快速连续点击“版本切换/音轨切换”确认不会出现卡死、黑屏或崩溃。
- 切换过程中返回上一页,确认无异常日志(特别是已销毁页面仍操作控制器的错误)。
- 长时间(10+ 分钟)反复切换字幕/版本/音轨,观察内存曲线应平稳,无明显持续上涨。
追加记录(剧集列表进度实时刷新)
- 问题现象:播放器内“剧集切换列表”进度依赖初次拉取的
UserData,播放中上报后没有本地同步,导致切换后进度不立即变化。 - 已修复:在播放页进度上报成功后,立即把当前剧集在本地列表中的进度状态同步更新(
lib/pages/video_player_page.dart)。 - 关键改动:
- 在
_updateProgress(...)成功分支中调用_syncCurrentEpisodeProgress(position, duration)。 - 新增
_syncCurrentEpisodeProgress(...):- 计算
PlayedPercentage(0~100); - 同步
UserData.PlayedPercentage、UserData.Played、UserData.PlaybackPositionTicks; - 仅在变化显著时触发
setState,避免无效高频刷新。
- 计算
- 在
- 效果:播放中或切集后,列表中的“在看进度/已看完成”状态可以更及时反映,不再明显滞后。
追加验证建议(进度刷新)
- 播放任一剧集约 30~60 秒后打开切换列表,确认当前集进度条有变化。
- 快进到接近结尾(>=95%)后,确认列表状态切为“已看”。
- 切换到其他剧集再返回,确认原剧集进度仍保持最新状态。
追加记录(Emby 会话上报与切集稳定性)
- 目标:修复 Emby 会话上报参数不准确、切音轨重建控制器带来的中断,以及切集收尾竞态(
lib/services/emby_api.dart、lib/pages/video_player_page.dart)。 - 进度上报修复:
updatePlaybackProgress(...)改为显式接收并上报真实MediaSourceId、稳定PlaySessionId、当前音轨/字幕索引、静音与音量状态。- 移除错误默认值(如固定
MediaSourceId=itemId、固定音轨/字幕、每次重新生成会话 ID)。
- 停止上报补齐:
stopPlayback(...)补齐可选参数:MediaSourceId、PlaySessionId、PositionTicks、AudioStreamIndex、SubtitleStreamIndex,并允许 204 返回。
- 收尾顺序化:
- 新增
_finalizePlaybackSession(),退出时按“最终 Progress -> Stopped -> dispose controller”顺序执行,避免并发 fire-and-forget 导致的会话尾部错乱。 - 增加
_isSessionFinalized/_isFinalizingSession防重,防止dispose()与切集路径重复上报。
- 新增
- 切集前先收尾:
- 新增
_switchToEpisodeWithFinalize(...),手动切集与自动下一集都会先完成当前会话顺序收尾,再pushReplacement。
- 新增
- 音轨切换优化:
_switchAudioStream(...)优先使用 fvp 轨道接口setAudioTracks([index]);- 若失败再回退到重建 controller,兼顾稳定与兼容。
追加记录(播放列表/切换 UI 修复)
- 修复右上角重叠:移除桌面端额外悬浮“播放列表按钮”,仅保留顶部控制栏入口,避免与视频格式标签重叠。
- 格式标签增加宽度约束与省略显示,避免长文本挤压右侧交互区。
- 切集提示层交互修复:
- 切换中遮罩由
IgnorePointer改为AbsorbPointer,防止点击透传造成误触。 - 切换中禁用剧集列表项点击,避免重复触发切集。
- 切换中遮罩由
- 轻量切换提示:
- 切换剧集时显示“正在切换内容...”提示;
- 增加
_isNavigatingToEpisode锁,防止快速连点导致多次导航。
追加验证建议(本轮)
- 在播放页右上角验证:视频格式标签与“切换剧集/系列内容”入口不再重叠。
- 在播放列表中快速连续点击不同剧集,确认只触发一次切换,且会出现“正在切换内容...”提示。
- 切集后在 Emby 会话侧观察:同一播放会话内
PlaySessionId稳定,MediaSourceId与当前版本一致。 - 退出播放页时观察日志/会话:先上报最终进度,再上报停止事件,无明显竞态异常。
追加记录(三端兼容与操作逻辑修复)
- 目标:修复桌面端、移动端、TV 端在同一套播放器手势/按键逻辑下的交互冲突与边界风险(
lib/pages/video_player_page.dart)。 - 平台判定修复:
- 新增
package:flutter/foundation.dart并引入_isDesktop状态。 didChangeDependencies()中改为“平台 + 屏幕尺寸”联合判定:_isDesktop:Windows/Linux/macOS;_isTV:Android 且width >= 960 && shortestSide >= 540;_isMobile:Android/iOS 且非 TV。
- 目的:避免仅靠
width < 600导致横屏手机被误判为非移动端。
- 新增
- 手势分层修复(桌面/移动):
- 当前策略已更新为
enableTouchGestures = !_isTV(非 TV 平台启用)。 - 双击分区快进、水平拖动进度、垂直滑动亮度/音量在 iPhone/iPad/Windows/macOS/Linux/Android 可用;TV 端保持禁用触摸手势。
- 主播放器手势层使用
HitTestBehavior.opaque提升整屏命中稳定性,减少手势丢失。
- 当前策略已更新为
- TV 焦点链路修复:
- 打开播放列表时,焦点索引改为安全初始化:当前集存在则用当前索引,不存在时回退到
0,空列表则置空。 - TV 打开列表后通过
requestFocus()显式聚焦_playlistFocusNode;关闭列表时unfocus()。 - 效果:遥控器上下键可稳定进入“列表焦点模式”,不再频繁误触发音量调整。
- 打开播放列表时,焦点索引改为安全初始化:当前集存在则用当前索引,不存在时回退到
- TV 按键边界保护:
select增加完整保护:仅在“列表已开 + 索引非空 + 列表非空 + 索引有效”时触发选集。arrowUp/arrowDown增加空列表保护,防止索引越界。arrowRight在列表已开但未聚焦时,自动请求列表焦点。- 效果:消除
-1索引或空列表场景的潜在崩溃风险。
- 质量检查:
- 已对
lib/pages/video_player_page.dart执行 lint 检查,未引入新增 lint 错误。
- 已对
追加验证建议(三端)
- 桌面端(Windows/macOS/Linux):
- 鼠标/触控手势应可触发亮度/音量滑动与双击快进;空格、方向键、Esc 交互保持可用。
- 播放列表通过顶部按钮开关,交互与键盘操作无冲突。
- 移动端(Android/iOS):
- 横屏手机下仍识别为移动端;双击左右快进、滑动调音量/亮度可用。
- 剧集场景下移动端侧边播放列表按钮按预期显示并可操作。
- TV 端(Android TV):
- 打开播放列表后,上下键可稳定移动焦点,确认键可选集中项。
- 空列表或当前集未命中时不崩溃;返回键在“先关列表、再退页面”路径下行为正确。
追加记录(操作逻辑设计收敛)
- 目标:解决“平台判定误伤平板、全屏状态语义不一致、主动退出收尾非阻塞”三类设计风险(
lib/pages/video_player_page.dart)。 - TV 判定收敛:
- 将 TV 判定改为 Android 且满足以下任一条件:
MediaQuery.navigationMode == NavigationMode.directional(遥控器方向键导航);- 或大屏阈值(
width >= 1100 && shortestSide >= 700)。
- 目的:降低 Android 平板在横屏下被误判为 TV 的概率。
- 将 TV 判定改为 Android 且满足以下任一条件:
- 全屏语义对齐:
- 初始化阶段系统 UI 模式改为
edgeToEdge,与_isFullScreen = false一致。 - 目的:避免“初始就沉浸式,但按钮显示未全屏”的状态错位。
- 初始化阶段系统 UI 模式改为
- 退出流程顺序化(主动路径):
- 新增
_confirmExitIfNeeded():播放中退出统一二次确认。 - 新增
_handleWillPop():确认后先await _finalizePlaybackSession(),再允许退出。 - 新增
_requestExitFromUi():用于顶部返回按钮、键盘Esc、TVgoBack/escape,统一走“确认 -> 顺序收尾 -> pop”流程。 - 目的:减少主动退出时最终进度/停止事件丢失的窗口。
- 新增
- 本轮质量结果:
lib/pages/video_player_page.dartlint 检查通过,无新增 lint 错误。
追加验证建议(设计收敛)
- Android 平板横屏:
- 触控操作应保持移动端路径(手势可用),不应误进入 TV 焦点模式。
- Android TV:
- 遥控器方向键导航应保持 TV 路径(列表焦点可移动,确认可选集)。
- 退出链路:
- 顶部返回、键盘
Esc、TV 返回键均应先触发确认(播放中)并完成顺序收尾再退出。
- 顶部返回、键盘
- 全屏按钮:
- 初始状态与图标语义一致;点击后系统 UI 模式切换与按钮状态同步。
追加记录(账号安全与连接提示改造)
- 目标:完成“密码不明文落盘 + 连接失败结构化提示”的首轮落地(
lib/services/server_manager.dart、lib/services/emby_api.dart、lib/pages/add_server_page.dart、lib/pages/edit_server_page.dart)。 - 存储安全改造:
- 在
ServerManager引入flutter_secure_storage,增加saveCredential/readCredential/deleteCredential接口。 SharedPreferences不再保存明文password,仅保存元数据与credentialKey。ServerInfo增加credentialKey字段,新增/更新/删除服务器时同步处理 secure storage 凭据生命周期。
- 在
- 旧数据迁移:
loadServers()检测到旧结构含password时自动迁移到 secure storage。- 迁移成功后从本地 JSON 删除
password并回写;迁移失败则保留旧值并记录告警,避免影响可用性。
- API 错误模型:
EmbyApiService新增ApiErrorType与ApiException,统一映射网络不可达、超时、SSL、认证失败、404、5xx 等错误。_request()与authenticate()改为抛结构化错误;页面层不再依赖字符串 contains 判断异常类型。
- UI 提示升级:
- 新增/编辑服务器页按错误类型展示“可读原因 + 下一步建议”,并统一“重试/返回编辑”交互。
- 覆盖场景:断网、超时、地址错误、证书异常、账号密码错误、服务器异常。
- 回归结果:
flutter analyze:本次改动文件无新增 lint error(工程内仍有既有 info/warning)。flutter test:现有默认widget_test.dart用例失败(与本次改造无直接关系,属既有测试模板不匹配现状)。
追加记录(Windows secure storage 构建兼容修复)
- 问题:Windows 构建报错
flutter_secure_storage_windows_plugin.cpp无法找到atlstr.h(环境缺少 ATL 头)。 - 处理策略:采用仓库内 vendoring +
dependency_overrides,避免要求开发机强制安装 ATL 组件。 - 已落地内容:
- 新增
third_party/flutter_secure_storage_windows(基于4.1.0)。 pubspec.yaml增加:dependency_overrides.flutter_secure_storage_windows -> path: third_party/flutter_secure_storage_windows
- 在 vendored 插件中移除 ATL 依赖:
- 删除
#include <atlstr.h>; - 用
MultiByteToWideChar/WideCharToMultiByte替换CA2W/CW2A。
- 删除
- 新增
- 影响评估:
- 仅替换 Windows 实现包来源,Dart API 与上层调用不变。
darwin/linux/web/platform_interface仍使用原来源包,非 Windows 端行为不应受该补丁影响。
- 验证:
flutter pub get成功,锁文件显示 windows 实现来自本地 path。flutter build windows --debug成功,atlstr.h构建失败已消除。
追加记录(连接失败白屏修复)
- 问题现象:当服务器不可达或鉴权失败导致首页分区数据均加载失败时,页面仅显示空内容区域,用户感知为“白屏”。
- 根因分析:
VideoListPage在各分区请求失败后会把分区数据置空并关闭 loading;- 但“全部分区为空”的场景没有全局错误占位 UI,导致界面无反馈。
- 修复方案(
lib/pages/video_list_page.dart):- 新增
_hadSectionLoadError标记,记录是否发生过分区级加载失败。 - 在
_loadViews/_loadLatestItems/_loadContinueWatching/_loadFavorites失败分支中统一置位该标记。 - 在
_loadAllSections()入口重置标记,避免重试后状态污染。 - 在
_buildBody()增加兜底渲染条件:- 当“无任何分区数据 + 无分区加载中 + 发生过加载失败”时,显示错误占位而非空白。
- 错误占位提供可操作入口:
重试、返回服务器列表。
- 新增
- 结果:
- 连接失败场景下不再白屏,用户可看到明确提示并执行下一步操作。
- 已对修改文件执行 lint 检查,无新增 lint 错误。
追加记录(全平台 UI/播放器视觉焕新)
- 目标:完成主题模块化、浏览页去硬编码、播放器控制层 tokens 化,并保持播放逻辑不变。
- 主题模块:
- 新增
lib/theme/app_theme.dart,提供buildAppTheme(...)与ThemeExtension<PlayerChrome)。 main.dart改为统一接入 light/dark 主题,保留原有启动顺序与fvp.registerWith(...)。
- 新增
- 顶栏与系统状态栏:
lib/widgets/adaptive_app_bar.dart改为根据brightness动态设置SystemUiOverlayStyle;AppBar/SliverAppBar前景色与图标色跟随主题,避免深色模式对比错误。
- 浏览页主题化:
lib/widgets/video_card.dart去除固定浅色与 Apple 字体依赖,标题/占位/进度改为ColorScheme/TextTheme。lib/pages/video_list_page.dart分区标题、错误态、加载指示、骨架屏颜色改为主题驱动。lib/pages/server_list_page.dart空状态、按钮、删除色与头像色改为主题色。
- 播放器视觉统一:
lib/pages/video_player_page.dart将顶部/底部渐变、胶囊控件、对话框选中色等改为读取PlayerChrome。lib/widgets/video_progress_slider.dart轨道/拇指/高亮由主题强调色驱动,替换固定红色。
追加记录(analyze 与 third_party 配置修复)
- 问题:根目录执行
flutter analyze时,third_party/flutter_secure_storage_windows子包配置引用package:very_good_analysis,在根工程解析不到,导致全仓库分析报错。 - 处理:
- 根
analysis_options.yaml增加analyzer.exclude: third_party/**; third_party/flutter_secure_storage_windows/analysis_options.yaml改为自包含最小配置,避免依赖子包 dev-only include。- 顺手移除
lib/pages/video_detail_page.dart未使用方法_showStreamSelectionDialog,减少无效告警。
- 根
- 结果:
- 全仓库
flutter analyze已不再出现 third_party 相关错误; - 当前剩余均为工程既有
info级提示(默认 analyze 在本环境仍可能返回非 0); flutter build windows --debug通过。
- 全仓库
追加记录(flutter_secure_storage_windows 示例依赖修复)
- 问题现象:在
third_party/flutter_secure_storage_windows/example执行flutter pub get失败,报flutter_secure_storage_platform_interface本地 path 不存在(..\..\flutter_secure_storage_platform_interface)。 - 根因:
example/pubspec.yaml中存在dependency_overrides,将flutter_secure_storage_platform_interface强制指向仓库内本地目录;但当前仓库未包含该目录。 - 最小修复:
- 删除
third_party/flutter_secure_storage_windows/example/pubspec.yaml中以下覆盖:dependency_overrides.flutter_secure_storage_platform_interface -> path: ../../flutter_secure_storage_platform_interface
- 保持其余依赖与版本约束不变,避免引入额外回归风险。
- 删除
- 验证结果:
- 在
third_party/flutter_secure_storage_windows/example目录执行flutter pub get成功; - 输出
Got dependencies!,未再出现 path 依赖缺失错误。
- 在
- 经验:
- 对 third_party 示例工程,优先避免指向仓库外/不存在目录的 path override;
- 若确需本地 path 依赖,应先确认目标目录随仓库同步存在,并在文档中标注用途。
追加记录(播放器亮度与手势跨平台修复,2026-04-23)
- 目标:修复“亮度手势有反馈但画面不变”与“部分平台手势不可用”的问题,确保在非 TV 平台均有可见亮度变化(
lib/pages/video_player_page.dart)。 - 背景:
- 原实现仅依赖
fvpController.setBrightness(...),在 iOS/iPadOS(及部分设备/渲染路径)可能不生效,导致亮度百分比变化但画面无变化。 - 历史逻辑将触摸手势限制为
_isMobile && !_isDesktop,导致 Windows 桌面端手势路径被关闭。
- 原实现仅依赖
- 本轮改动:
- 新增亮度视觉兜底:
- 增加
_brightnessOverlayColor(),按_brightness计算黑/白叠层透明度(中性点 0.5;变暗使用黑层,变亮使用白层)。 - 视频渲染节点改为
Stack(VideoPlayer + ColoredBox),叠层使用IgnorePointer,不拦截手势。
- 增加
- 兜底启用策略收敛:
_needsBrightnessOverlayFallback调整为!_isTV(非 TV 平台启用兜底),避免平台差异导致“有手势但无画面变化”。
- 手势可用性修复:
enableTouchGestures从_isMobile && !_isDesktop调整为!_isTV,恢复 iPhone/iPad/Windows 的手势通路。- 主播放器
GestureDetector增加behavior: HitTestBehavior.opaque,提升整屏命中稳定性,减少手势丢失。
- 控制器重建后亮度一致性:
- 在初始化与重建控制器路径后统一补调
_applyPlayerBrightnessEffect(_brightness),避免切换版本/音轨/字幕后亮度状态丢失。
- 在初始化与重建控制器路径后统一补调
- 新增亮度视觉兜底:
- 变更结果:
- 手势调节(亮度/音量)在非 TV 平台恢复可触发;
- 亮度调节在底层 API 失效时仍有可见画面变化;
- 不改变 TV 端遥控器焦点交互路径。
- 质量检查:
ReadLints(lib/pages/video_player_page.dart)无新增 lint 错误。flutter analyze仍存在工程既有info提示;本轮未引入新增 error。
冲突声明(以最新为准)
- 若本节与历史“亮度实现”记录存在冲突(如“仅调用系统/底层亮度 API 即可覆盖所有平台”等表述),一律以本次“非 TV 平台统一启用画面叠层兜底 + 控制器重建后重套亮度”的实现为准。