Flutter/OpenHarmony 三方库适配实战:just_audio 音频播放
一、项目信息
| 项目 | 内容 |
|---|---|
| 仓库名称 | just_audio |
| 仓库地址 | https://atomgit.com/oh-flutter/just_audio |
| 基于版本 | just_audio 0.10.5 |
| 适配 TAG | 0.10.5-ohos-1.0.4 |
| OHOS 插件包 | just_audio内置ohos实现 |
| OHOS 原生入口 | JustAudioOhosPlugin |
| 原生播放器 | MediaAvPlayer |
| 主方法通道 | com.ryanheise.just_audio.methods |
just_audio提供播放器、播放列表、进度流、缓冲状态和音频源抽象。OHOS 适配的
重点是让这些概念对应到AVPlayer生命周期,同时保留原库的 Dart API。
二、通道设计
原库使用“主通道创建播放器,每个播放器独立方法和事件通道”的结构:
JustAudioOhosPlugin | +-- com.ryanheise.just_audio.methods | init / disposePlayer / disposeAllPlayers | +-- AudioPlayer(id) | +-- com.ryanheise.just_audio.methods.<id> +-- com.ryanheise.just_audio.events.<id> +-- com.ryanheise.just_audio.data.<id> | +-- MediaAvPlayer | +-- @ohos.multimedia.media.AVPlayer主通道只管理播放器实例的创建和销毁,具体load、play、pause、seek、setVolume等命令由每个播放器自己的通道处理。这样多个播放器之间不会串号。
三、插件注册与生命周期
插件声明在主just_audio/pubspec.yaml中:
flutter:plugin:platforms:ohos:pluginClass:JustAudioOhosPlugin原生插件创建主方法通道,并监听引擎重启:
exportdefaultclassJustAudioOhosPluginimplementsFlutterPlugin{onAttachedToEngine(binding:FlutterPluginBinding):void{constcontext=binding.getApplicationContext();constmessenger=binding.getBinaryMessenger();this.methodCallHandler=newMainMethodCallHandler(context,messenger);this.channel=newMethodChannel(messenger,'com.ryanheise.just_audio.methods',);this.channel.setMethodCallHandler(this.methodCallHandler);this.lifecycleListener={onPreEngineRestart:()=>{this.methodCallHandler?.dispose();},}asEngineLifecycleListener;binding.getFlutterEngine().addEngineLifecycleListener(this.lifecycleListener,);}}热重启时必须先释放所有底层播放器,否则旧AVPlayer会继续占用音频焦点和媒体
资源。引擎销毁时还需要移除监听器、解绑 MethodChannel 并清理全局 Context。
四、创建播放器时等待原生初始化
主处理器使用播放器 id 管理实例:
case'init':{constid=call.argument('id')asstring;if(this.players.has(id)){result.error('Platform player '+id+' already exists',null,null);break;}constplayer=newAudioPlayer(this.messenger!,id);awaitplayer.waitForInitialization();this.players.set(id,player);result.success(null);break;}MediaAvPlayer的构造函数不能等待异步初始化,因此它保存初始化 Promise:
constructor(){this.initPromise=this.initAudioPlayer();}asyncwaitForInitialization():Promise<void>{awaitthis.initPromise;if(this.initError!=null){thrownewError(this.initError.message);}}主通道在init响应前等待这个 Promise,避免 Dart 端刚收到创建结果就调用load
时底层avPlayer仍为null。
五、每个播放器的通道与事件
播放器构造时创建三条通道:
this.methodChannel=newMethodChannel(messenger,'com.ryanheise.just_audio.methods.'+id,);this.eventChannel=newBetterEventChannel(messenger,'com.ryanheise.just_audio.events.'+id,);this.dataEventChannel=newBetterEventChannel(messenger,'com.ryanheise.just_audio.data.'+id,);BetterEventChannel保存当前EventSink。没有 Dart 监听器时不发送事件,取消
订阅后清空 sink:
onListen(args:Object,eventSink:EventSink):void{this.eventSink=eventSink;}onCancel(args:Object):void{this.eventSink=null;}播放器方法通道支持加载、播放、暂停、音量、速度、跳转和播放列表调整:
switch(call.method){case'load':awaitthis.load(call,result);break;case'play':awaitthis.play(result);break;case'pause':this.pause();result.success(newMap<String,Object>());break;case'setVolume':this.setVolume(call.argument('volume')asnumber);break;case'setSpeed':this.setSpeed(call.argument('speed')asnumber);break;case'seek':awaitthis.seek(call.argument('positionMs')asnumber,call.argument('index')asnumber|null,result,);break;}跳转操作需要等待异步完成并回执,否则 Dart 的seek()Future 可能永久等待。
六、AVPlayer 状态与错误
MediaAvPlayer将AVPlayer状态机转换为播放器状态:
- 初始化阶段等待
avPlayer创建完成。 load设置播放源并等待initialized/prepared。- 缓冲事件更新 buffered position。
- 完成事件将处理状态改为 completed。
- 错误事件通过 EventChannel 抛回 Dart。
初始化失败必须明确上报:
onErrorCallback(err:BusinessError<void>):void{this.sendError(err.code?.toString()??err.name,err.message,);}如果加载失败但不发送错误,Dart 端的load()可能一直处于 loading。适配中特别
处理了未 prepare 完成时的播放器操作错误,使其转换为可读错误;已 prepare 后的
时序错误则记录日志,避免正常切歌流程触发误报。
七、音频源处理
适配层支持网络 URL、本地文件、Asset 和播放列表来源。本地文件使用文件描述符,
避免将路径字符串交给不支持该路径的播放器接口。
MediaSource负责将 Dart 参数转换为具体播放源。加载时根据progressive、hls、dash、concatenating、clipping、looping等类型创建不同数据结构。
播放列表操作通过concatenatingInsertAll、concatenatingRemoveRange和concatenatingMove更新底层列表。索引变化后必须同步当前播放项、初始位置和
shuffle 顺序。
八、Dart 使用示例
网络音频播放:
finalplayer=AudioPlayer();awaitplayer.setUrl('https://example.com/audio.mp3');awaitplayer.setVolume(0.8);awaitplayer.setSpeed(1.25);player.playbackEventStream.listen((event){debugPrint('position=${event.position}, state=${event.processingState}');});awaitplayer.seek(constDuration(seconds:30));awaitplayer.pause();awaitplayer.dispose();加载本地文件:
awaitplayer.setFilePath('/data/storage/el2/base/files/audio.wav');网络播放需要宿主 HAP 声明:
{ module: { requestPermissions: [ { name: 'ohos.permission.INTERNET' }, ], }, }页面销毁时必须调用dispose(),不能只暂停播放器。暂停不会释放AVPlayer、音频
焦点和所有事件监听。
九、构建、测试与结果
执行:
flutter analyze --no-pub lib fluttertest结果:
- 静态分析无问题。
- 70 个 Dart 测试通过。
- 示例 HAP 构建、安装和启动成功。
十、遗留问题
- 截图不能替代不同采样率、编码格式和网络抖动的真机测试。
- 音频焦点、蓝牙耳机切换和来电中断需要系统级场景验证。
- 后台播放需要宿主应用配置长时任务和相关权限,不能只依赖插件。
- 播放列表、Clipping、DASH/HLS 组合场景应继续补充集成测试。
十一、代码提交与协作
欢迎加入 CPF-Flutter 鸿蒙社区,参与 Flutter
OHOS 经验交流。