news 2026/10/22 7:36:08

Flutter/OpenHarmony 三方库适配实战:video_player 视频播放

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter/OpenHarmony 三方库适配实战:video_player 视频播放

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.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:VideoPlayerPlugin

Dart 注册类设置平台实例:

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的主要流程是:

  1. 通过TextureRegistry注册纹理并取得 texture id。
  2. 根据 Asset、网络 URI 或 fd 准备播放源。
  3. 使用AVMetadataExtractor或AVImageGenerator读取首帧和媒体信息。
  4. 将首帧 PixelMap 设置为纹理背景,避免初始化前出现纯黑区域。
  5. 创建VideoPlayer和对应 EventChannel。
  6. 调用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 经验交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 7:32:24

Java实用类

一、枚举类型枚举&#xff1a;由一组固定的常量组成的类型。使用enum关键字定义定义&#xff1a;public enum WeekDay {Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday }使用&#xff1a;public class T01 {public static void test(WeekDay day){switch (day){cas…

作者头像 李华
网站建设 2026/10/22 7:32:20

2026粵港澳高管報讀內地EMBA人脈攻略:如何借力復旦大學國際金融學院EMBA打通大灣區與全國產融鏈?

當香港、深圳、廣州及澳門的高管與企業家希望借助內地商學院EMBA拓展大灣區高端人脈與跨境商業網絡時&#xff0c;越來越清醒地認識到&#xff1a;高質量的人脈從來不是靠「換名片」換出來的&#xff0c;而是靠「資本互補、產業協同與課題共創」沉澱出來的。 在內地頂尖高校的…

作者头像 李华
网站建设 2026/10/22 7:30:51

Flutter学习day1--环境搭建与Dart基础

Flutter学习day1–环境搭建与Dart基础 想入门 Flutter&#xff0c;第一关不是 Widget&#xff0c;而是两件事&#xff1a;把环境跑起来、把 Dart 的基础语法理顺。本文从环境搭建讲到 Dart 变量声明的三个关键字&#xff0c;再用官方计数器 demo 拆解 Flutter 应用的最小骨架&a…

作者头像 李华
网站建设 2026/10/22 7:29:34

【AI大模型】显存复用:多任务共享模型显存的方法

【AI大模型】显存复用:多任务共享模型显存的方法 多个任务共用同一个模型时,若每个任务各占一份显存,显存随任务数线性膨胀,很快耗尽。显存复用的核心是让同一模型的权重只加载一份,KV Cache 按任务动态划分与回收,把显存占用从任务数解耦出来。本文给出完整方法:先讲显…

作者头像 李华
网站建设 2026/10/22 7:29:27

DTU 和 LoRa 模块有什么区别?工业无线回传怎么选

工业现场做无线数据回传&#xff0c;采购清单上经常同时出现 DTU 和 LoRa 模块两类设备&#xff0c;报价、配套方式与调试工作量差别不小。若把两者当成同一种东西比价&#xff0c;容易出现两种偏差&#xff1a;要么买了 LoRa 却发现数据送不到平台&#xff0c;要么为每个点位都…

作者头像 李华
网站建设 2026/10/22 7:27:51

小白程序员也能掌握的AI大模型应用开发,高薪岗位等你来!

本文强调了AI大模型应用开发工程师的重要性&#xff0c;这类人才能够将AI技术落地到企业业务中&#xff0c;获得高薪职位。文章指出&#xff0c;企业需求的是能够结合模型和业务的人才&#xff0c;而非单纯训练模型。随着AI技术的发展&#xff0c;应用开发、Agent系统、多模态系…

作者头像 李华