Flutter/OpenHarmony 三方库适配实战:video_player 视频播放
一、项目信息
| 项目 | 内容 |
|---|---|
| 仓库名称 | video_player |
| 仓库地址 | https://atomgit.com/oh-flutter/video_player |
| 基于版本 | video_player 2.11.1 |
| 适配 TAG | video_player-v2.11.1-ohos-1.0.2 |
| OHOS 插件包 | video_player_ohos |
| OHOS 原生入口 | VideoPlayerPlugin |
| Dart 注册类 | OhosVideoPlayer |
| 原生播放器 | @ohos.multimedia.media.AVPlayer |
| 视频显示方式 | FlutterTexture |
video_player的公开 API 由VideoPlayerController提供,平台差异集中在VideoPlayerPlatform。OHOS 适配只需要实现同一套平台契约:
create创建播放器并返回 texture/player id。play、pause、seekTo、setVolume控制播放。videoEventsFor返回初始化、缓冲、播放状态和播放完成事件。buildView返回可嵌入 Flutter 页面的视频画面。dispose释放播放器和原生纹理资源。
二、整体架构
本次适配采用 Pigeon 生成的强类型通道:
VideoPlayerController | v VideoPlayerPlatform | v OhosVideoPlayer(Dart 实现) | v OhosVideoPlayerApi(Pigeon BasicMessageChannel) | v VideoPlayerApiImpl | v AVPlayer + Flutter TextureRegistry | v 每个播放器独立 EventChannel使用 Pigeon 而不是手写 Map 参数,可以减少字段名、空值和类型转换错误。播放控制
通过 Pigeon 通道,播放状态通过 EventChannel 流式返回,两者职责清晰。
三、OHOS 插件声明
video_player_ohos/pubspec.yaml将 OHOS 包声明为video_player的平台实现:
name:video_player_ohosversion:1.0.0+2flutter:plugin:implements:video_playerplatforms:ohos:dartPluginClass:OhosVideoPlayerpackage:io.flutter.plugins.videoplayerpluginClass:VideoPlayerPluginDart 注册类设置平台实例:
classOhosVideoPlayerextendsVideoPlayerPlatform{OhosVideoPlayer({OhosVideoPlayerApi?pluginApi}):_api=pluginApi??OhosVideoPlayerApi();finalOhosVideoPlayerApi_api;staticvoidregisterWith(){VideoPlayerPlatform.instance=OhosVideoPlayer();}}业务继续使用:
finalcontroller=VideoPlayerController.asset('assets/video.mp4');awaitcontroller.initialize();awaitcontroller.play();业务代码不会感知底层是AVPlayer还是 Texture。
四、统一不同数据源
Dart 层把 Asset、网络地址和本地文件转换为CreateMessage:
Future<int?>_create(DataSourcedataSource)async{String?asset;String?uri;switch(dataSource.sourceType){caseDataSourceType.asset:asset=dataSource.asset;break;caseDataSourceType.network:uri=dataSource.uri;break;caseDataSourceType.file:uri='fd://${awaitVideoPlayerOhosChannel.getFileFdByPath(dataSource.uri!,)}';break;default:uri=dataSource.uri;}return_api.create(CreateMessage(asset:asset,uri:uri,httpHeaders:dataSource.httpHeaders,viewType:PlatformVideoViewType.textureView,));}本地文件不会把 DartFile对象传给原生层,而是通过辅助通道打开文件并返回文件
描述符。原生侧使用文件描述符创建播放器:
asynconMethodCall(call:MethodCall,result:MethodResult):Promise<void>{if(call.method==='getFileFdByPath'){try{constfilePath=call.argument('filePath')asstring;constfile=fs.openSync(filePath);result.success(file.fd);}catch(e){result.success(-1);}}}网络地址直接交给AVPlayer,请求头通过播放器配置传入。
五、创建 AVPlayer 和 Flutter Texture
原生插件在 Ability 绑定阶段创建VideoPlayerApiImpl:
onAttachedToAbility(binding:AbilityPluginBinding):void{if(this.videoPlayerApiImpl==null){this.videoPlayerApiImpl=newVideoPlayerApiImpl(this.flutterState!,binding,);this.videoPlayerApiImpl.setup(this.pluginBinding!.getBinaryMessenger());}}VideoPlayerApiImpl.create的主要流程是:
- 通过
TextureRegistry注册纹理并取得 texture id。 - 根据 Asset、网络 URI 或 fd 准备播放源。
- 使用
AVMetadataExtractor或AVImageGenerator读取首帧和媒体信息。 - 将首帧 PixelMap 设置为纹理背景,避免初始化前出现纯黑区域。
- 创建
VideoPlayer和对应 EventChannel。 - 调用
createAVPlayer()并等待播放器进入可用状态。
创建成功后返回的 texture id 同时作为 FlutterTexture的 id:
@overrideWidgetbuildView(int playerId){returnTexture(textureId:playerId);}因此 Flutter 页面可以直接嵌入原生视频画面,并继续使用普通 Widget 控制播放按钮、
进度条和音量。
六、播放控制
VideoPlayerApiImpl根据 playerId 找到原生播放器并调用:
play(playerId:number):void{constvideoPlayer=this.videoPlayers.get(playerId);videoPlayer?.play();}pause(playerId:number):void{constvideoPlayer=this.videoPlayers.get(playerId);videoPlayer?.pause();}seekTo(playerId:number,positionMs:number):void{constvideoPlayer=this.videoPlayers.get(playerId);videoPlayer?.seekTo(positionMs);}setVolume(playerId:number,volume:number):void{constvideoPlayer=this.videoPlayers.get(playerId);videoPlayer?.setVolume(volume);}所有方法都需要检查 playerId 是否存在。页面销毁与异步控制可能并发,直接使用
非空对象会导致应用崩溃。
七、事件流
每个播放器创建独立事件通道:
flutter.io/videoPlayer/videoEvents<textureId>Dart 侧把原生 Map 转换为标准VideoEvent:
@overrideStream<VideoEvent>videoEventsFor(int playerId){return_eventChannelFor(playerId).receiveBroadcastStream().map((dynamicraw){finalevent=rawasMap<dynamic,dynamic>;switch(event['event']){case'initialized':returnVideoEvent(eventType:VideoEventType.initialized,duration:Duration(milliseconds:(event['duration']asnum).toInt(),),size:Size((event['width']asnum).toDouble(),(event['height']asnum).toDouble(),),);case'completed':returnVideoEvent(eventType:VideoEventType.completed);case'bufferingStart':returnVideoEvent(eventType:VideoEventType.bufferingStart);case'bufferingEnd':returnVideoEvent(eventType:VideoEventType.bufferingEnd);default:returnVideoEvent(eventType:VideoEventType.unknown);}});}事件通道按 texture id 隔离,可以避免多个播放器互相收到错误的初始化或完成事件。
八、资源释放
播放器销毁必须同时释放原生播放器和纹理:
dispose(playerId:number):void{constvideoPlayer=this.videoPlayers.get(playerId);if(videoPlayer==null){return;}videoPlayer.release();this.flutterState?.getTextureRegistry().unregisterTexture(playerId);this.videoPlayers.remove(playerId);}Ability 分离或引擎销毁时,插件会清理全部播放器:
onDetachedFromEngine(binding:FlutterPluginBinding):void{this.disposeAll();}onDetachedFromAbility():void{this.disposeAll();}disposeAll同时解绑辅助 MethodChannel 并清空实现对象。业务页面也应在dispose()中释放VideoPlayerController。
九、Dart 使用示例
classVideoDemoextendsStatefulWidget{constVideoDemo({super.key});@overrideState<VideoDemo>createState()=>_VideoDemoState();}class_VideoDemoStateextendsState<VideoDemo>{latefinalVideoPlayerController_controller;@overridevoidinitState(){super.initState();_controller=VideoPlayerController.network('https://example.com/video.mp4',httpHeaders:const<String,String>{'Authorization':'Bearer token',},);_controller.initialize().then((_){_controller.setLooping(true);_controller.play();setState((){});});}@overrideWidgetbuild(BuildContextcontext){if(!_controller.value.isInitialized){returnconstCircularProgressIndicator();}returnAspectRatio(aspectRatio:_controller.value.aspectRatio,child:VideoPlayer(_controller),);}@overridevoiddispose(){_controller.dispose();super.dispose();}}网络播放还需要宿主 HAP 声明:
{ module: { requestPermissions: [ { name: 'ohos.permission.INTERNET' }, ], }, }十、测试结果
执行:
flutter analyze --no-pub libtestfluttertest结果:
- 静态分析无问题。
- 17 个 Dart 测试通过。
- 示例 HAP 构建、安装和启动成功。
- Asset、Remote 和 LocalFile 三种数据源入口均显示。
十一、遗留问题
- 模拟器只验证基础播放 UI 和本地/网络数据源入口,编码兼容性仍需真机批量验证。
- DRM、HDR、画中画和复杂音轨切换应按业务场景单独测试。
- 仓库中部分旧视频资源是 Git LFS 指针,本次远端快照不上传这些大文件,测试时应
使用业务自己的小体积视频或重新准备测试资产。 - 后台播放和音频焦点行为需要根据宿主应用的后台配置验证。
十二、代码提交与协作
欢迎加入 CPF-Flutter 鸿蒙社区,参与 Flutter
OHOS 经验交流。