Flutter/OpenHarmony 三方库适配实战:audioplayers 多播放器
一、项目信息
| 项目 | 内容 |
|---|---|
| 仓库名称 | audioplayers |
| 仓库地址 | https://atomgit.com/oh-flutter/audioplayers |
| 基于版本 | audioplayers 6.5.1 |
| 适配 TAG | 6.5.1-ohos-1.0.1 |
| OHOS 插件包 | audioplayers_ohos 5.2.1 |
| OHOS 原生入口 | AudioplayersPlugin |
| 原生播放器 | WrappedPlayer + MediaPlayerPlayer |
| 实例方法通道 | xyz.luan/audioplayers |
| 全局方法通道 | xyz.luan/audioplayers.global |
audioplayers支持多个播放器实例同时存在,公开 API 包括音源设置、播放、暂停、
跳转、音量、倍速、事件流和音频上下文。OHOS 适配需要在保留这些语义的同时处理
多个AVPlayer、音频焦点、后台任务和资源释放。
二、插件架构
本次适配采用实例通道与全局通道分离的结构:
audioplayers Dart | +-- xyz.luan/audioplayers | create / setSource / resume / pause / seek / dispose | +-- xyz.luan/audioplayers.global | init / setAudioContext / global log / global error | +-- xyz.luan/audioplayers.global/events | v AudioplayersPlugin | +-- HashMap<playerId, WrappedPlayer> | +-- 每个播放器独立 EventChannel | +-- MediaPlayerPlayer / SoundPoolPlayer | +-- @ohos.multimedia.media.AVPlayer实例命令通过playerId找到对应播放器,全局命令管理默认音频上下文、日志和全局
错误。每个播放器拥有独立事件通道,多个播放器同时播放时不会互相串流。
三、插件声明与初始化
audioplayers_ohos/pubspec.yaml:
name:audioplayers_ohosversion:5.2.1flutter:plugin:implements:audioplayersplatforms:ohos:package:xyz.luan.audioplayerspluginClass:AudioplayersPlugin原生插件在引擎附加时创建通道和 SoundPool 管理器:
onAttachedToEngine(binding:FlutterPluginBinding):void{this.context=binding.getApplicationContext();this.binaryMessenger=binding.getBinaryMessenger();this.soundPoolManager=newSoundPoolManager(this);this.methods=newMethodChannel(this.binaryMessenger,'xyz.luan/audioplayers',);this.globalMethods=newMethodChannel(this.binaryMessenger,'xyz.luan/audioplayers.global',);this.globalEvents=newEventHandler(newEventChannel(this.binaryMessenger,'xyz.luan/audioplayers.global/events',),);}Ability 分离时暂停所有正在播放的播放器并停止后台长时任务;引擎销毁时释放所有
实例、SoundPool 管理器和事件通道。
四、创建播放器与事件通道
实例通道收到create后创建独立播放器和事件通道:
if(call.method==='create'){constplayerId=call.argument('playerId')asstring;consteventHandler=newEventHandler(newEventChannel(this.binaryMessenger!,`xyz.luan/audioplayers/events/${playerId}`,),);this.players.set(playerId,newWrappedPlayer(this,eventHandler,this.defaultAudioContext.copy(),this.soundPoolManager!,),);response.success(1);return;}事件通道会发送 prepared、duration、complete、seek complete、current position、
log 和 error。位置更新使用定时器,只在存在实际播放中的播放器时启动:
startUpdates():void{this.stopUpdates();this.updateInterval=setInterval(()=>{this.updateCallback();},200);}stopUpdates():void{if(this.updateInterval!=null){clearInterval(this.updateInterval);this.updateInterval=null;}}没有播放任务后停止定时器,可以避免空闲页面持续消耗计时器资源。
五、方法映射
实例命令按 Dart API 映射到底层播放器:
switch(call.method){case'setSourceUrl':awaitplayer.setSource(newUrlSource(call.argument('url')asstring,call.argument('isLocal')asboolean,),);break;case'setSourceBytes':player.setSource(newBytesSource(call.argument('bytes')asArrayBuffer),);break;case'resume':awaitthis.startContinuousTask();player.play();break;case'pause':player.pause();awaitthis.stopContinuousTask();break;case'stop':awaitplayer.stop();break;case'seek':player.seek(call.argument('position')asnumber);break;case'setVolume':player.setVolume(call.argument('volume')asnumber);break;case'setPlaybackRate':player.setRate(call.argument('playbackRate')asnumber);break;case'dispose':player.dispose();this.players.remove(playerId);break;}未知或文本 MIME 不能作为音频播放。URL 音源会先按扩展名判断 MIME,文本文件会
通过错误事件返回,而不是让AVPlayer抛出难以理解的底层错误。
六、URL 和字节流音源
URL 音源区分远程和本地:
setForMediaPlayer(mediaPlayer:media.AVPlayer):void{if(this.isLocal){constfile=fs.openSync(this.url);mediaPlayer.url=`fd://${file.fd}`;}else{mediaPlayer.url=this.url;}}本地文件通过 fd 加载,远程地址交给AVPlayer。字节流音源使用AVDataSrcDescriptor提供读取回调:
constsource:media.AVDataSrcDescriptor={fileSize:this.bytes.byteLength,callback:(buffer,length,position)=>{if(position==null||position>=source.fileSize){return-1;}constcopyLength=Math.min(length,this.bytes.byteLength-position,);newUint8Array(buffer).set(newUint8Array(this.bytes.slice(position,position+copyLength)),);returncopyLength;},};mediaPlayer.dataSrc=source;低延迟 SoundPool 当前不支持任意字节流,这种组合会明确抛出不支持错误。
七、音频焦点与后台任务
播放器通过FocusManager管理音频会话。获得焦点后播放;临时失去焦点时暂停并
标记恢复状态;永久失去焦点时停止等待自动恢复:
maybeRequestAudioFocus(player:WrappedPlayer):void{conststrategy:audio.AudioSessionStrategy={concurrencyMode:this.getFocusStrategy(),};this.audioSessionManager.activateAudioSession(strategy).then(()=>player.actuallyPlay()).catch((err:BusinessError)=>{Log.e(TAG,`activate failed:${err.code},${err.message}`);});}后台播放通过 AVSession 和长时任务实现:
this.session=awaitAVSessionManager.createAVSession(context,'audioplayers','audio',);awaitthis.session.activate();awaitbackgroundTaskManager.startBackgroundRunning(context,backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,wantAgentObj,);宿主 HAP 必须配置:
{ module: { abilities: [ { backgroundModes: ['audioPlayback'], }, ], requestPermissions: [ { name: 'ohos.permission.KEEP_BACKGROUND_RUNNING', reason: '$string:background_running_reason', usedScene: { abilities: ['EntryAbility'], when: 'inuse', }, }, ], }, }同时需要在资源文件中提供权限用途文案。未配置长时任务时,切后台后播放可能被系统
回收。
八、Dart 使用示例
多个实例播放:
finalbackground=AudioPlayer(playerId:'background');finaleffect=AudioPlayer(playerId:'effect');awaitbackground.setSourceUrl('https://example.com/background.mp3',isLocal:false,);awaitbackground.setReleaseMode(ReleaseMode.loop);awaitbackground.play();awaiteffect.setSourceAsset('sounds/click.wav');awaiteffect.setVolume(0.6);awaiteffect.play();effect.onPlayerComplete.listen((_){effect.dispose();});内存字节流:
finalplayer=AudioPlayer();finalbytes=awaitrootBundle.load('assets/audio.wav');awaitplayer.setSourceBytes(bytes.buffer.asUint8List());页面销毁时依次停止并释放播放器:
awaitplayer.stop();awaitplayer.dispose();九、构建、测试结果
验证环境:
- Flutter:
3.44.9+ohos-0.0.1-canary1 - OHOS SDK:
26 - DevEco Studio:
26.0.0 - 模拟器:
emulator 7.0.0.107(SP8DEVC00E999R4P11)
验证内容:
- 适配包
flutter analyze --no-pub通过。 - 主包
flutter analyze --no-pub通过。 - 22 个 Dart 测试通过。
- 示例 HAP 构建、安装和启动成功。
十、遗留问题
setBalance、音效、空间音频和不同音频焦点策略仍需按系统版本验证。- HLS、低延迟直播和复杂网络切换需要真机测试。
- 后台播放依赖宿主 HAP 的长时任务和权限配置。
audioplayers_android_exo中部分.gitignore是循环符号链接,构建工具链扫描
全仓库时应排除该目录。- 示例中的
icudtl.dat较大,本次远端快照不提交该构建资源。
十一、代码提交与协作
欢迎加入 CPF-Flutter 鸿蒙社区,参与 Flutter
OHOS 经验交流。