ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter for OpenHarmony音乐播放器MV播放实现与踩坑记录

Flutter for OpenHarmony音乐播放器MV播放实现与踩坑记录 这篇继续聊咱们那个“Flutter for OpenHarmony”音乐播放器App的实战记录第19期主题很直接MV播放实现。前面18篇已经把工程搭建、本地音乐扫描、播放器内核、后台播放、歌词滚动都拆完了一遍这会儿产品侧提了新需求——歌曲详情页要加“播放MV”入口点进去能看视频能暂停、拖进度、旋转全屏退出之后最好是“怎么进来、怎么回去”进MV前歌在放退出后歌还能继续放。这需求听起来就像“接一个视频播放器的事”但真在OpenHarmony上做一遍就会发现坑比想象中多。MV播放对音乐App来说是一次典型的“跨界”视频解码依赖硬件编解码能力和系统媒体组件页面生命周期要处理好否则退出页面后视频声音还在跑更麻烦的是它跟已有的音频播放器抢“声音通道”一旦不处理MV声音和后台音乐同时响体验直接翻车。这篇文章我会从功能定位、依赖接入、播放页实现、全屏旋转、音频协调这几个维度完整拆一遍最后把这段时间在真机上调MV遇到的各种坑梳理成清单。如果你也在做Flutter for OpenHarmony应用或者正在给播放器加类似功能这篇应该能帮你少折腾不少时间。1. MV播放模块的整体定位与三个边界设计1.1 从产品需求反推技术形态先别急着写代码产品给的需求往往不完整需要自己把场景补全。我们最终落地的交互是这样歌曲详情页和播放页里如果这首歌曲存在关联MV就显示一个“观看MV”的入口按钮点击后跳转到独立的MV播放页播放页默认竖屏展示16:9视频点击右下角全屏按钮自动切横屏并隐藏系统状态栏退出全屏则回到竖屏从播放页返回时恢复进入之前的音频播放状态。这里有两个关键判断。第一MV播放页必须是一个独立的路由页面不能塞在歌曲详情页里直接内嵌播放。我们最早图省事想的是详情页封面区域下方放一个小的视频预览用户划过去就能看。结果在RK3568板子上跑起来后问题非常大详情页本身有封面转动、歌词渲染、推荐歌单列表Flutter布局频繁变化视频纹理在页面树里一直存活解码器也一直占着用户滑动列表时视频区域会出现卡顿、闪黑块甚至首帧迟迟出不来。后来把所有视频逻辑挪到独立页面进出都有明确的初始化/销毁时机问题少了一大半。第二个判断是要明确这套模块的功能边界。MV播放本质上就是一个“单路视频点播”产品不会要求你在MV里做倍速、音轨切换、弹幕这些复杂能力所以技术上要尽量轻量别一上来就引一个大而全的播放器SDK。我们的边界就是播一个在线视频、显示加载状态、暂停/播放、拖动进度、全屏、退出恢复。砍掉一切不需要的能力后续维护和排查都会轻松很多。1.2 技术选型为什么我选了video_player_ohos这条路在OpenHarmony上用Flutter做视频播放我实际调研下来大致有三条路。第一条是直接用Flutter官方生态里的video_player插件然后适配到OpenHarmony。社区里确实有OpenHarmony分支的video_player_ohos插件它底层封装的是系统AVPlayer播放能力把解码后的画面通过纹理方式送到Flutter渲染层上层API设计基本对齐原版video_player。项目代码迁移成本很低原来写过video_player的同事几乎不需要重新学习。第二条路是找通用的多媒体播放SDK比如一些商业播放器。这类SDK在Android/iOS上很强但OpenHarmony上的适配成熟度参差不齐有些还停留在Demo阶段引入后反而要花大量时间解决插件自身的问题不太适合我们这种需要稳定交付的音乐播放器App。第三条路是自己用PlatformChannel写插件直接调OpenHarmony系统的AVPlayer能力。这条路对团队能力要求极高视频纹理怎么交给Flutter去渲染生命周期怎么跟路由绑定播放器状态怎么回传硬解失败怎么降级这些都是隐藏的工作量。优点是灵活可控缺点是开发周期长、维护门槛高。如果你团队里没有熟悉OpenHarmony媒体框架的底层开发我建议别在这条路上死磕。我最终选择了video_player_ohos核心考虑就两个一是它解的是“有没有Flutter侧可用的视频播放器”这个问题二是它在OpenHarmony官方适配的Flutter环境中能比较稳定地跑通。对大部分做OpenHarmony应用层的团队来说先站在插件肩膀上把业务跑起来远比从零造轮子更高效。1.3 一个容易被忽略的边界应用内音频协调MV播放还有一个很重要但容易忽略的设计问题进入MV后原来正在播放的音乐到底怎么处理。从用户直觉来说点开MV那一刻背景音乐肯定要停因为视频本身有声音两路声音同时输出是非常糟糕的体验。但这不只是“暂停一下音乐”那么简单还涉及退出MV后要不要恢复、什么时候恢复、如果用户在前面把音乐暂停了又该怎么处理。这个模块不能写死在MV页面里需要在播放器服务层设计一个暂停恢复机制。我们后面单独用一整章讲这个这里先在设计上定个原则MV播放器不直接操纵音乐播放器的内部状态而是通过一个统一的服务去请求暂停并在合适的时机请求恢复。2. OpenHarmony上接入video_player_ohos的正确姿势2.1 环境准备先对齐系统版本和设备硬件在写代码之前得先明确运行环境。我自己项目的主力调试设备是RK3568开发板系统是OpenHarmony 4.0 ReleaseFlutter用的是官方配套OpenHarmony的ohos分支。为什么要强调这个因为视频播放比音频播放更依赖底层硬件能力同一个视频在RK3568和RK3588上的解码表现可能差别很大哪怕是RK3568不同厂家出的板子设备树不一样灌进去的系统镜像和屏幕驱动不同Flutter应用的渲染结果都可能不同。做这一类功能前先把“系统版本 开发板型号 Flutter版本”这个三位一体记录下来会省掉很多玄学问题。另外有一点要提醒Flutter工程的ohos目录结构是OpenHarmony那套跟普通Android工程不一样。视频播放涉及网络请求和媒体会话配置权限的时候要去对应ohos模块的module.json5文件里操作。如果MV视频源是访问http地址而不是https还涉及到明文流量许可的配置。这些不要只在文档里确认一定要在真机上验证一遍因为不同版本的工具链对配置的处理策略有差异。2.2 最小依赖集pubspec.yaml里该加什么我的实际用法是在pubspec.yaml里只加一个视频插件不额外堆叠UI封装库。代码如下dependencies: flutter: sdk: flutter # OpenHarmony专用的video_player实现 video_player_ohos: ^1.0.0要注意的是如果你的工程里之前因为某些原因已经引了标准版video_player需要先移除或者做包名隔离否则两个插件同时存在编译期很容易出现方法通道重复注册或者符号冲突的问题。我们在项目早期就踩过一次代码里 import 了video_player_ohos但pubspec里还残留着video_player传递依赖最后在OpenHarmony设备上运行时控制台报了一串插件冲突错误排查了很久才定位到是依赖冗余。在OpenHarmony的工程配置里还需要确认网络权限已经加上module.json5中类似这样{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }如果漏了这一步症状很典型播放器初始化一直不回调成功或者首帧一直不出现但工程不报编译错。2.3 先把最小Demo跑通再谈业务在动手做MV完整页面之前我先在项目里拉了一个最小的测试页面目的是确认插件在当前这台RK3568上能不能正常播视频。测试代码非常简单一个StatefulWidget初始化时创建一个网络视频控制器然后把它渲染出来import package:flutter/material.dart; import package:video_player_ohos/video_player_ohos.dart; class SimpleVideoDemo extends StatefulWidget { const SimpleVideoDemo({super.key}); override StateSimpleVideoDemo createState() _SimpleVideoDemoState(); } class _SimpleVideoDemoState extends StateSimpleVideoDemo { late VideoPlayerController _controller; bool _initialized false; override void initState() { super.initState(); _controller VideoPlayerController.networkUrl( Uri.parse(https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4), ); _controller.initialize().then((_) { setState(() { _initialized true; }); _controller.play(); }); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(MV Video Demo)), body: Center( child: _initialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) : const CircularProgressIndicator(), ), ); } override void dispose() { _controller.dispose(); super.dispose(); } }这里有两个小细节值得说。第一_controller.initialize()是个异步过程返回的Future成功之后才表示视频宽高、时长等参数可用此时才能用_controller.value.aspectRatio去算界面比例。在未初始化完成之前直接拿value里的aspectRatio经常拿到的是0.0或者默认值画面就会变形甚至不显示。第二dispose方法里一定要调用controller.dispose()否则每次进出页面都会残留一个底层播放器实例连续进出十几次之后应用可能会因为解码器占满而出现播放失败。跑这个最小Demo时我建议先播公开的https测试视频源确认插件本身没问题再换成自己的业务MV链接。如果公开源能播、自己的源播不了那问题多半在MV链接的编码格式、防盗链或CDN访问策略上。3. MV数据模型与播放页的完整实现3.1 MV数据从哪里来怎么建模MV页面涉及的数据其实没有想象中复杂我的Dart侧模型只保留了播放页真正关心的字段。具体代码类似class MvItem { final String mvId; final String title; final String artist; final String coverUrl; final String playUrl; final String? hdPlayUrl; const MvItem({ required this.mvId, required this.title, required this.artist, required this.coverUrl, required this.playUrl, this.hdPlayUrl, }); }在真实项目里这个MvItem一般由歌曲详情页的数据源拼装出来可能来自服务端接口也可能来自本地缓存。我个人习惯是MV的播放地址在进入播放页前就解析好通过构造函数直接传给页面不在页面内部再做一次异步网络请求去拿播放地址。这样做的好处是页面职责单一它只负责播放和交互不关心数据从哪来。数据加载失败应该在入口处就拦截并提示而不是等用户跳进播放页后面对一块黑屏。3.2 从歌曲详情页正确跳转到MV播放页MV入口点击之后跳转方式要稍微想想。如果只是一个普通push那么从MV页面全屏旋转回竖屏后音乐播放器页面的方向可能已经被系统记住了下一次跳出来就乱了。我们现在的做法是使用Navigator.push但是配合一个Future在页面返回后统一恢复竖屏方向。Futurevoid _openMvPage(BuildContext context, MvItem item) async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); if (!context.mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (_) MvPlayPage(mvItem: item), ), ); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); }这里在跳转前先强制一下竖屏是为了防止用户之前停留在横屏界面进入MV页面后方向状态错乱。MV页面内部自己负责处理横屏和全屏切换返回时外层再兜底一次竖屏这样能把全局方向状态的影响控制到最小。3.3 播放页状态管理核心字段不能乱MvPlayPage必须是一个StatefulWidget因为它需要管理播放器控制器、加载状态、控制栏显隐、全屏模式等多个状态。我列一下这个页面里最核心的状态字段class _MvPlayPageState extends StateMvPlayPage with WidgetsBindingObserver { late VideoPlayerController _controller; bool _initializing true; bool _controlsVisible true; bool _userPaused false; bool _wasMusicPlayingBefore false; Timer? _hideControlsTimer; MvScreenMode _screenMode MvScreenMode.portrait; }_initializing表示控制器是否初始化完成初始化没完成之前视频区域要显示加载圈。_controlsVisible控制底部控制栏和顶部返回区域的显示用户点击屏幕就切换同时开启一个3秒自动隐藏的定时器。_userPaused记录用户是否主动按过暂停这个字段后面配合页面生命周期管理特别重要。_wasMusicPlayingBefore则用来记住进入MV前音乐播放器的状态退出时决定是否恢复。WidgetsBindingObserver是为了监听应用前后台切换因为这个坑我们在测试时翻车过应用切到桌面或者切到后台时OpenHarmony系统并不一定会立刻暂停Flutter侧的媒体播放如果不自己处理视频声音还会继续响几秒甚至一直响。代码里处理方式通常是这样override void didChangeAppLifecycleState(AppLifecycleState state) { if (state AppLifecycleState.paused) { if (_controller.value.isPlaying) { _controller.pause(); } } else if (state AppLifecycleState.resumed) { if (!_userPaused !_controller.value.isPlaying) { _controller.play(); } } }注意恢复播放前一定要判断_userPaused否则会出现“用户手动暂停了MV切了一下后台再回来结果它自己又开始播”的诡异现象。3.4 播放器初始化MV要的是稳定首帧而不是花式功能播放器的初始化放在initState里做。对MV这个场景我建议把循环播放关掉保持默认的播完即停因为MV通常是“看一遍”的观看心智视频播完如果自动从头再来体验反而奇怪。initState里的核心逻辑如下override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _pauseBackgroundMusicIfNeeded(); _controller VideoPlayerController.networkUrl( Uri.parse(widget.mvItem.playUrl), ); _controller.initialize().then((_) { if (!mounted) return; setState(() { _initializing false; }); _controller.play(); _userPaused false; _startHideControlsTimer(); }).catchError((Object e) { if (!mounted) return; setState(() _initializing false); _showInitError(); }); }_pauseBackgroundMusicIfNeeded这个方法的细节放在后面第5章讲。initialize成功之后调用play同时启动一个定时器作用是在几秒后自动隐藏控制栏让画面保持干净。播放页里最忌讳的是初始化刚结束就把所有UI堆在屏幕上进度条、按钮一直不消失观感非常像Demo不像正式产品。如果初始化失败一定要给用户一个明确提示。常见的失败原因是网络不通、地址403、视频编码不支持等。我们不能让页面卡在无限加载的转圈状态里至少得有一个“加载失败请重试”的按钮。3.5 视频画面如何排版竖屏下的16:9适配竖屏状态下MV播放页的布局是顶部安全区下方是视频区域视频区域按照16:9比例占满宽度视频区域下方放标题和艺人再往下是控制操作区。实现时可以直接用AspectRatio和VideoPlayer组合SizedBox( width: double.infinity, child: AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ), )很多人会忽略一个点视频源的宽高比不一定严格等于16:9比如可能是1:1或者4:3。如果直接用AspectRatio去撑视频画面会出现拉伸变形因为AspectRatio是按照外部布局的宽高来强制约束的。要保证视频不变形应该使用FittedBox加AspectRatio双层包裹让FittedBox负责缩放适配。这里的细节在横屏全屏时尤其明显后面第4章还会展开。3.6 进度条与控制交互的渐进式实现MV页的控制栏包含三部分顶部返回按钮、中间主操作按钮暂停/播放、底部进度条和时间文本。这部分UI直接堆在一个Stack里通过点击屏幕切换透明度即可。进度条的刷新是播放页最容易卡顿的地方。我最初的做法是写一个定时器每秒setState一次去更新当前播放位置结果在RK3568上效果很差拖动进度条时整个页面掉帧明显视频画面也跟着卡。后来把进度条部分单独抽出来用VideoProgressIndicator组件实现VideoProgressIndicator( _controller, allowScrubbing: true, colors: VideoProgressColors( playedColor: Colors.redAccent, bufferedColor: Colors.white38, backgroundColor: Colors.white24, ), )VideoProgressIndicator内部监听控制器的时间变化局部刷新不会触发整个页面重建这是最稳妥的写法。如果需要显示“当前时间/总时间”文本再单独用一个StatefulWidget去监听即可不要跟整个页面状态混在一起。播放按钮的切换逻辑也简单核心就是根据_controller.value.isPlaying来判断当前状态点击时调用play或pause并更新_userPaused标记。遇到不响应触摸的问题时优先检查Stack层级是不是后面的页面容器把视频Surface盖住了这在OpenHarmony的纹理渲染下经常出现不是按钮代码写错了。4. 全屏沉浸、横屏旋转与退出恢复4.1 真正横屏全屏还是页面内“假全屏”做全屏前要先拍板一件事切到横屏后是要系统也跟着旋转让页面真正变成横屏布局还是只是在竖屏页面里把视频区域放大到全屏也就是“假全屏”。我最终选了“真横屏”方案因为MV观看场景与大屏沉浸需求更匹配。具体来说在全屏按钮点击后先把系统PreferredOrientations切成横屏再隐藏系统状态栏导航栏然后重新调整页面布局。代码如下Futurevoid _enterFullScreen() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); if (!mounted) return; setState(() { _screenMode MvScreenMode.fullscreen; }); }退出全屏则反着来Futurevoid _exitFullScreen() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); if (!mounted) return; setState(() { _screenMode MvScreenMode.portrait; }); }这里有个细节设置Orientation和SystemUiMode都是异步操作在await回来之后一定要判断mounted否则用户可能已经在这个间隙返回了上一个页面再setState就会报错。这种问题在真机上不是必现但偶尔出现一次就够烦的了。4.2 全屏后视频如何铺满FittedBox和AspectRatio的配合进入横屏模式后视频不能再沿用竖屏页面的“宽度满屏、高度自适应”策略而是要让它填满整个屏幕。然而直接让视频SizedBox.expand铺满如果视频比例和屏幕比例不一致画面会变形。我们最后用的方案是Stack的最底层放一个黑色背景Container然后让视频区域通过FittedBox控制适配策略。如果需要“铺满不留黑边”用BoxFit.cover但会裁掉部分画面一般MV观看我们用的还是BoxFit.contain因为MV是完整内容裁掉边缘会影响观看。竖屏页面同理。这里还有一个容易踩的坑如果用SizedBox.expand直接包VideoPlayer在某些OpenHarmony系统上视频纹理可能会出现拉伸后刷新区域不准确的问题表现为视频画面只占屏幕左上角一块。解决办法是不要直接对VideoPlayer做expend而是先包一个AspectRatio再用FittedBox放大SizedBox.expand( child: FittedBox( fit: BoxFit.contain, child: SizedBox( width: _controller.value.size.width, height: _controller.value.size.height, child: VideoPlayer(_controller), ), ), )这样VideoPlayer的原始像素尺寸不变FittedBox只负责整体缩放OpenHarmony纹理层不容易出现刷新错乱。4.3 安全区、刘海屏和屏幕圆角横屏全屏时要处理安全区。竖屏播放页里顶部状态栏、底部手势条需要避开所以一般会套SafeArea。但是全屏横屏之后视频本身是应该真正贴边显示的不能因为设备有刘海就把画面缩小。控制栏可以避让但视频画面不能避让。实现时我的做法是Stack最底层是全屏视频这个区域不套SafeArea顶部返回栏和底部控制栏分别使用SafeArea包裹让它们各自留出系统安全距离。如果只图省事在视频区域外整个套一层SafeArea那么横屏时视频就会被“安全区”挤成一个带黑边的小矩形虽然也能播但观感非常业余。刘海屏设备的具体表现要把真机插上去验证尤其是横屏方向下左右两侧的安全区数据。开发板通常没有刘海但手机上切横屏时MediaQuery的padding值会变化UI构建一定要依赖MediaQuery动态取不要写死。4.4 返回键行为和退出时机MV播放页的返回处理比普通页面复杂因为用户可能在竖屏播放态按返回也可能在全屏横屏态按返回。我们期望的行为是如果当前处于全屏模式按返回键或点返回按钮先退出全屏、恢复竖屏不直接退出页面如果当前已经是竖屏模式再按返回才退出MV播放页。这里需要拦截系统返回键在Flutter里用PopScope包裹页面然后根据_screenMode状态做出不同响应。PopScope的onPopInvokedWithResult回调里可以根据当前模式决定是直接处理还是继续返回。dispose时要做的事情非常关键。除了基础调用controller.dispose()我们还需要把系统方向恢复成竖屏、移除生命周期监听、必要情况下恢复后台音乐播放状态。顺序不对会出现方向没恢复或者黑屏残留的问题。我自己的顺序是先暂停视频再恢复方向再恢复音乐最后dispose播放器并且所有这些操作放在dispose或者路由pop之前集中处理。原因是视频还在播的时候直接dispose可能会因为底层解码线程正在输出画面而崩溃或闪黑屏。5. MV与音乐播放器的音频协调机制5.1 不只要暂停音乐还要记录“进入前状态”MV是带声音的视频进入后如果不处理音频播放器就会出现两路声音同时输出的问题。很多人会想那就直接在MV页面initState里调用全局播放器的pause方法呗。但这个方案有个漏洞。假设用户进入MV前音乐本来就处于暂停状态那他进MV看完出来预期是什么音乐应该继续保持暂停状态而不是“因为进了一次MV返回后突然自己开始播”。所以进入MV前一定要记录当时的播放状态退出时按这个记录决定是否恢复。我在PlayerService里提供了一个轻量的控制接口MV页面的initState里这样用void _pauseBackgroundMusicIfNeeded() { final player PlayerService.instance; _wasMusicPlayingBefore player.isPlaying; if (player.isPlaying) { player.pause(); } }退出时判断void _restoreBackgroundMusicIfNeeded() { final player PlayerService.instance; if (_wasMusicPlayingBefore !player.isPlaying) { player.play(); } }这里多了一个判断如果退出MV后音乐播放器已经在播放了就不要重复调用play。这个情况是存在的比如用户在MV页面多任务切换时通过通知栏点了一下音乐播放器的播放键如果我们直接按照_wasMusicPlayingBefore去恢复反而会打断用户刚才的新操作。5.2 区分“主动暂停”和“被动暂停”防止误恢复做恢复逻辑时最大的坑是“恢复”和“用户主动暂停”之间的关系。用户进入MV后可能在MV页里点了暂停键那么退出MV时音乐要不要继续从产品体验上我认为应该继续恢复你进入前的状态因为MV页里的暂停是对MV视频的暂停不是对音乐播放器的暂停。所以MV页面里的_userPaused变量不应该影响音乐恢复逻辑音乐恢复只取决于_wasMusicPlayingBefore。反过来有一种情况不能恢复进入MV前音乐在放但在MV播放过程中因为来电、闹钟、音频焦点被其他应用抢占等原因音乐播放器被系统或者外部服务暂停了。这时候退出MV如果无条件调play去恢复音乐可能会跟当前正在发声的其他应用抢焦点体验很差。这个层级的问题单靠Flutter侧的简单判断处理不了最好在PlayerService里加一个“暂停原因”标记区分业务暂停和外部焦点抢占。我在项目里的处理是如果暂停原因是外部焦点抢占MV退出时不主动恢复而是等服务层的焦点释放回调来恢复。这一条虽然有点超纲但对真正要做上线级音乐App的人来说迟早会碰到。5.3 播放器页面里不要直接持有PlayerService的单例依赖Flutter开发里有一个惯性思维在哪里用到就引哪里的全局单例。MV播放页里如果直接import PlayerService的实例虽然简单但会把这个页面和播放器服务生命周期严重耦合后面想给MV模块做独立测试就很难。我们目前的做法是给回调函数或者通过构造参数传入MediaController接口MV页面只依赖这个抽象接口。真实项目里如果战线拉得长建议把“暂停音乐”和“恢复音乐”做成两个可注入的函数页面内只管调用不关心具体实现。这一层的解耦会让后续维护舒服很多尤其是当你决定把MV播放能力抽出来给其他模块复用时会发现这是最值回票价的一步。6. 高频问题排查与避坑清单6.1 真机调试中最常见的八类问题这段时间在OpenHarmony真机上调MV播放我把遇到的高频问题整理成了一张速查表方便出问题时直接对号入座。现象可能原因排查方向与解决办法视频一直黑屏但音频正常视频编码不被当前设备硬解支持或网络源加载失败先用系统播放器播放该链接确认编码格式替换成H.264/AAC的MP4测试检查网络权限和明文HTTP配置点击入口后页面转圈不回调success/errorMV地址解析异常或底层初始化未完成确认控制器使用networkUrl播放地址是完整可访问的URL真机打开日志查看底层AVPlayer状态拖动进度条后出现花屏或绿屏硬件解码器在seek后状态未完全重建切换清晰度或重新调用initialize将默认解码策略调整为更兼容的模式退出MV页面后系统方向仍停留在横屏返回时没有恢复PreferredOrientations在dispose里统一恢复竖屏并在路由返回Future后再次兜底设置视频上面的控制按钮有时点不中视频纹理Surface盖住了Flutter控件层级不要直接用Overlay去盖视频控制栏和视频放在同一个Stack内保持层级清晰应用切后台后MV声音还在播放没有监听AppLifecycleState页面加入WidgetsBindingObserver在paused时暂停播放器连续进出MV页面多次后播放失败播放器控制器没有正确dispose确保dispose中调用controller.dispose并置空引用检查是否存在异步回调在页面销毁后继续操作控制器视频画面拉伸变形AspectRatio使用不当使用FittedBox包一层AspectRatio和VideoPlayer确保等比缩放6.2 一个值得单独记录的RK3568解码案例有一次我们在一台RK3568开发板上测试一个用户反馈的MV高清版本视频地址是H.265编码的MKV封装。结果无论怎么调视频始终黑屏但日志显示播放器一直在正常推进度。后来把同一个文件转成H.264的MP4后不到两秒就出了画面。这个案例的教训是OpenHarmony设备的硬件VPU对编码格式的支持并不统一RK3568和RK3588的解码能力差异很大。不要假设服务端给的视频源在用户设备上都一定能播尤其当你的App面向多种OpenHarmony设备时最好在服务端下发MV地址时带上编码格式信息或者准备多码率多编码的兼容源客户端在播放失败时能自动降级或提示。对于现在的音乐播放器App来说MV视频源尽量统一压制为H.264 AAC的MP4H.265可以先放一放后续清晰度分级需求出现时再做兼容策略。6.3 热重载与调试中的特别提醒Flutter开发免不了用热重载。普通页面热重载问题不大但涉及video_player这类底层带纹理渲染的页面热重载后经常会出现播放画面卡死、视频区域黑块的情况。这不是业务代码写错了而是底层原生播放器实例和Flutter侧状态在热重载后无法正确同步。所以调MV页面时我基本不用热重载每次改完代码直接热重启甚至冷启动一次更稳妥。你如果在用这个插件开发遇到“刚热重载完视频放不了”这类问题不用浪费时间查代码先重启再试。另外一个调试建议是遇到视频初始化慢或首帧黑屏问题时多看看系统侧日志查找包含AVPlayer、MediaCodec、VideoSurface相关关键字的信息。Flutter侧的日志往往只能告诉你“初始化失败”这个结果真正的原因在系统媒体模块日志里。开发阶段可以把日志等级调低真机上跑一遍完整流程比在代码里瞎打点高效得多。6.4 关于稳定性的几点个人体会MV播放做完到现在整体稳定性已经比较让人安心了。如果让我总结这套模块最核心的经验其实是三句话视频页面一定要独立进出生命周期要理清不要在主页面到处setState局部刷新才能保证视频流畅音频协调不能只在UI层打补丁服务层要提前预留暂停恢复机制。最后分享一个小技巧在播放器页面调试时我习惯在App的Debug模式里加一个隐藏的视频源切换入口平时不显示需要排查时点四下封面就能弹出一个对话框直接填一个新的MV地址去播放。这个入口在真机联调、测试反馈“某个视频播不了”的时候特别有用不用重新打包几秒钟就能确认问题是出在播放器本身还是视频源强烈建议有条件的项目加上。
返回列表