
Flutter/OpenHarmony 三方库适配实战video_player 视频播放一、项目信息项目内容仓库名称video_player仓库地址https://atomgit.com/oh-flutter/video_player基于版本video_player 2.11.1适配 TAGvideo_player-v2.11.1-ohos-1.0.2OHOS 插件包video_player_ohosOHOS 原生入口VideoPlayerPluginDart 注册类OhosVideoPlayer原生播放器ohos.multimedia.media.AVPlayer视频显示方式FlutterTexturevideo_player的公开 API 由VideoPlayerController提供平台差异集中在VideoPlayerPlatform。OHOS 适配只需要实现同一套平台契约create创建播放器并返回 texture/player id。play、pause、seekTo、setVolume控制播放。videoEventsFor返回初始化、缓冲、播放状态和播放完成事件。buildView返回可嵌入 Flutter 页面的视频画面。dispose释放播放器和原生纹理资源。二、整体架构本次适配采用 Pigeon 生成的强类型通道VideoPlayerController | v VideoPlayerPlatform | v OhosVideoPlayerDart 实现 | v OhosVideoPlayerApiPigeon 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.02flutter:plugin:implements:video_playerplatforms:ohos:dartPluginClass:OhosVideoPlayerpackage:io.flutter.plugins.videoplayerpluginClass:VideoPlayerPluginDart 注册类设置平台实例classOhosVideoPlayerextendsVideoPlayerPlatform{OhosVideoPlayer({OhosVideoPlayerApi?pluginApi}):_apipluginApi??OhosVideoPlayerApi();finalOhosVideoPlayerApi_api;staticvoidregisterWith(){VideoPlayerPlatform.instanceOhosVideoPlayer();}}业务继续使用finalcontrollerVideoPlayerController.asset(assets/video.mp4);awaitcontroller.initialize();awaitcontroller.play();业务代码不会感知底层是AVPlayer还是 Texture。四、统一不同数据源Dart 层把 Asset、网络地址和本地文件转换为CreateMessageFutureint?_create(DataSourcedataSource)async{String?asset;String?uri;switch(dataSource.sourceType){caseDataSourceType.asset:assetdataSource.asset;break;caseDataSourceType.network:uridataSource.uri;break;caseDataSourceType.file:urifd://${awaitVideoPlayerOhosChannel.getFileFdByPath(dataSource.uri!,)};break;default:uridataSource.uri;}return_api.create(CreateMessage(asset:asset,uri:uri,httpHeaders:dataSource.httpHeaders,viewType:PlatformVideoViewType.textureView,));}本地文件不会把 DartFile对象传给原生层而是通过辅助通道打开文件并返回文件描述符。原生侧使用文件描述符创建播放器asynconMethodCall(call:MethodCall,result:MethodResult):Promisevoid{if(call.methodgetFileFdByPath){try{constfilePathcall.argument(filePath)asstring;constfilefs.openSync(filePath);result.success(file.fd);}catch(e){result.success(-1);}}}网络地址直接交给AVPlayer请求头通过播放器配置传入。五、创建 AVPlayer 和 Flutter Texture原生插件在 Ability 绑定阶段创建VideoPlayerApiImplonAttachedToAbility(binding:AbilityPluginBinding):void{if(this.videoPlayerApiImplnull){this.videoPlayerApiImplnewVideoPlayerApiImpl(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的 idoverrideWidgetbuildView(int playerId){returnTexture(textureId:playerId);}因此 Flutter 页面可以直接嵌入原生视频画面并继续使用普通 Widget 控制播放按钮、进度条和音量。六、播放控制VideoPlayerApiImpl根据 playerId 找到原生播放器并调用play(playerId:number):void{constvideoPlayerthis.videoPlayers.get(playerId);videoPlayer?.play();}pause(playerId:number):void{constvideoPlayerthis.videoPlayers.get(playerId);videoPlayer?.pause();}seekTo(playerId:number,positionMs:number):void{constvideoPlayerthis.videoPlayers.get(playerId);videoPlayer?.seekTo(positionMs);}setVolume(playerId:number,volume:number):void{constvideoPlayerthis.videoPlayers.get(playerId);videoPlayer?.setVolume(volume);}所有方法都需要检查 playerId 是否存在。页面销毁与异步控制可能并发直接使用非空对象会导致应用崩溃。七、事件流每个播放器创建独立事件通道flutter.io/videoPlayer/videoEventstextureIdDart 侧把原生 Map 转换为标准VideoEventoverrideStreamVideoEventvideoEventsFor(int playerId){return_eventChannelFor(playerId).receiveBroadcastStream().map((dynamicraw){finaleventrawasMapdynamic,dynamic;switch(event[event]){caseinitialized:returnVideoEvent(eventType:VideoEventType.initialized,duration:Duration(milliseconds:(event[duration]asnum).toInt(),),size:Size((event[width]asnum).toDouble(),(event[height]asnum).toDouble(),),);casecompleted:returnVideoEvent(eventType:VideoEventType.completed);casebufferingStart:returnVideoEvent(eventType:VideoEventType.bufferingStart);casebufferingEnd:returnVideoEvent(eventType:VideoEventType.bufferingEnd);default:returnVideoEvent(eventType:VideoEventType.unknown);}});}事件通道按 texture id 隔离可以避免多个播放器互相收到错误的初始化或完成事件。八、资源释放播放器销毁必须同时释放原生播放器和纹理dispose(playerId:number):void{constvideoPlayerthis.videoPlayers.get(playerId);if(videoPlayernull){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});overrideStateVideoDemocreateState()_VideoDemoState();}class_VideoDemoStateextendsStateVideoDemo{latefinalVideoPlayerController_controller;overridevoidinitState(){super.initState();_controllerVideoPlayerController.network(https://example.com/video.mp4,httpHeaders:constString,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 鸿蒙社区参与 FlutterOHOS 经验交流。