做 Flutter for OpenHarmony 三方库适配,flutter_sound 绝对是绕不过去的一个硬骨头。这库在 Flutter 社区里几乎等同于音频播放和录音的默认选项,但它的原生实现默认只覆盖 Android 和 iOS。想让它跑在 OpenHarmony 设备上,不能靠改几个方法名,得把音频服务、插件生命周期、资源文件访问和事件回调整条链路重新走一遍。这篇文章就用 flutter_sound 的播放能力做主线,把适配思路、核心实现和实操中值得留意的坑都拆开讲。适合已经跑通过基础 Flutter 应用、准备啃三方库适配的开发者。
先交代一下背景。适配 Flutter 插件去 OpenHarmony,本质上是把原本写给 Android/iOS 的那一层原生桥接,重新翻译成 OpenHarmony 平台认识的语言。Dart 层不需要大动,真正花力气的是平台通道另一头的实现。flutter_sound 又是音频插件里覆盖范围比较大的一个,播放、录音、流式处理样样都有,天然适合拿来当进阶案例研究。下面按我实际会走的路线来写。
1. 为什么要把 flutter_sound 拉到 OpenHarmony 上?
1.1 OpenHarmony 的 Flutter 生态现状
Flutter for OpenHarmony 这个分支项目已经可以让 Dart 层代码跑在 OpenHarmony 设备上,UI 渲染、状态管理、Dart 插件机制都有了基础。但三方库生态远没有 Android/iOS 那么完整。很多 Flutter 包在 pub.dev 上看起来人畜无害,一引入工程就爆 MissingPluginException,原因很简单:插件的原生实现只写了 Android 目录和 iOS 目录,OpenHarmony 上没人接话。
这类问题的解法不是去改写 Dart 层,而是给插件补一块 OpenHarmony 平台实现。补完之后,Dart 层调用的 MethodChannel 还是那个 MethodChannel,事件通道还是那个事件通道,只是另一端从 MediaPlayer 换成了 OpenHarmony 自己的多媒体组件。
音频库在这个过程里属于典型目标。因为音频能力是基础功能,几乎每个应用都会用到,但音频又偏偏是系统绑定比较深的那类能力,权限、焦点、生命周期、编解码全都要操心。适配完一个 flutter_sound,等于把插件适配最常见的难点都过了一遍。
1.2 音频库适配为什么值得单独研究
如果你只适配过一个类似“弹个 Toast”的插件,那完全体会不到音频库的复杂程度。Toast 插件拿到字符串,调一下系统能力,完事。音频播放不是这样,它至少涉及几层:
- 资源从哪来:本地文件、asset 资源、网络流,三种来源三种处理方式;
- 权限声明:播放网络音频要 INTERNET,录音要 MICROPHONE;
- 音频焦点:闹钟响、来电时要不要暂停当前音频;
- 生命周期:页面销毁、插件 detach 时播放器要释放;
- 回调通知:播放进度、播放完成、错误状态要通过事件通道推回 Dart 层。
任何一环漏掉,表现都是“有时候能响、有时候不响”的玄学问题。所以我觉得,音频插件适配是最适合拿来练习 OpenHarmony 三方库改造的素材。搞明白这一条链路,后面再碰视频播放、摄像头预览这类插件,思路会顺很多。
2. 适配前先读懂 flutter_sound 的家底
2.1 源码里的模块划分与通道注册
动工之前,建议先把 flutter_sound 的源码结构过一遍。它按 Flutter 生态常见的 Federated Plugin 方式拆了多层,我们最需要关注的是 platform interface 和原生实现这两部分。
在标准实现里,播放器对应的平台通道大致会注册这样几个方法:
- startPlayer:开始播放,可以传文件路径、URL、asset 地址或内存数据;
- stopPlayer:停止播放;
- pausePlayer / resumePlayer:暂停和恢复;
- seekToPlayer:跳转进度;
- setVolume:设置音量;
- getPlayerState:查询播放状态。
除了方法通道,通常还会注册一个 EventChannel,用来持续往外推播放进度和播放完成事件。Dart 层通过这些通道跟原生实现通信,通道名在几个版本里有变化,最稳妥的做法是在你下载的源码里搜 “MethodChannel” 和 “EventChannel” 关键字,把名字记下来。
这里有个容易被忽略的细节:flutter_sound 并不是只有一个通道,播放器和录音器经常各走各的通道。适配时如果只改了播放相关的 method,录音功能仍然会挂。所以第一步最好画一张清单,把源码里所有注册过的通道名、方法名、参数格式全部列出来,越细越好。
2.2 从原生实现反推适配需求
看到通道清单之后,下一步是打开 Android 和 iOS 的实现文件,看每个 method 在原生侧到底做了什么。这个过程不是让你抄代码,而是把行为映射到 OpenHarmony 的对应能力上。我用一个表格来梳理常见的对应关系:
| flutter_sound 能力 | Android 原生实现 | OpenHarmony 适配可选方案 |
|---|---|---|
| 本地文件播放 | MediaPlayer | AVPlayer |
| 网络音频播放 | MediaPlayer 设置网络 URL | AVPlayer 直接设置网络 URL |
| asset 资源播放 | AssetManager 打开 fd | 将 asset 拷贝到应用缓存目录 |
| 录音采集 | AudioRecord | AudioCapturer |
| 音频焦点处理 | AudioManager 请求焦点 | AudioStreamManager 的中断监听 |
| 播放进度/完成回调 | Handler 定时上报 | AVPlayer 状态监听 + 定时查询 |
为什么要用 AVPlayer 而不是 AudioRenderer?因为 AVPlayer 把解封装、解码、播放状态机都封装好了,一首 mp3 传进去,prepare 完就能播。AudioRenderer 是面向低延迟 PCM 播放的,你还要自己完成解码,成本高得多。flutter_sound 的播放场景以文件和网络流为主,用 AVPlayer 是性价比最高的路线。
2.3 选型:直接改源码还是搞联邦插件
确认完能力映射,下一个问题是怎么动手。一般有两条路。
第一条路,直接改 flutter_sound 源码,在插件工程里新增一个 ohos 平台目录,把 OpenHarmony 实现写进去。好处是直观,改动范围可控,适合公司内部 SDK 或者个人项目快速验证。
第二条路,把 flutter_sound 改成真正的联邦插件,platform interface 独立成一个包,OpenHarmony 实现包单独发布。好处是符合开源插件生态的规范,后续别人可以直接复用,但前期工程量会大不少,需要重新组织包结构。
我给的建议是别一上来就搞联邦插件。第一次适配,先直接改源码验证链路通不通,等所有方法都跑通了,再考虑抽架构的事。文章后面的步骤都基于第一条路来讲。
3. 给 flutter_sound 补上 OpenHarmony 平台实现
3.1 工程改造与插件注册
拿到 flutter_sound 源码后,首先在插件工程根目录下新建一个 ohos 目录,里面放 OpenHarmony 侧的实现模块。这个目录的结构和你接触过的其他 Flutter 插件平台目录类似,核心就是一个实现类加一个入口注册。
入口类要做的第一件事,是告诉 Flutter 引擎“我是这个插件”。在 OpenHarmony 侧,插件需要实现对应的 Plugin 接口,并在被挂载时注册 MethodChannel。以常见写法为例,大致长这样:
import { MethodChannel } from '@ohos.flutter/bridge'; import { Plugin, PluginContext } from '@ohos.flutter/plugin'; export default class FlutterSoundPlugin implements Plugin { private context: PluginContext | undefined; onAttach(context: PluginContext): void { this.context = context; const channel = new MethodChannel(context, 'flutter_sound_player'); channel.setMethodCallHandler((call, result) => { this.handleMethodCall(call, result); }); } onDetach(context: PluginContext): void { // 释放资源,释放播放器 } }这段代码里 channel 名不保证和当前版本完全一致,但注册机制就是这个套路。你需要根据自己的源码版本调整。注意 onDetach 一定不能空着,播放器漏释放是后面内存问题的根源。
插件类写完之后,还要在工程配置里声明权限。打开对应模块的 module.json5,至少要加上网络访问权限;如果你还要验证录音能力,麦克风权限也要一起加:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.MICROPHONE" } ] } }麦克风属于敏感权限,光声明还不够,运行时需要动态申请。这块我在后面录音部分再细说。
3.2 播放链路:从 MethodChannel 到 AVPlayer
播放能力是整个适配的核心。先从方法分发说起。handleMethodCall 里要根据 call.method 做分支,把 Dart 传来的请求转给 OpenHarmony 的音频服务。一个简化的播放器实现思路如下:
import { AVPlayer, AVPlayerState } from '@ohos.multimedia.media'; async function startPlayer(path: string): Promise<void> { const avPlayer = await AVPlayer.create(getContext()); avPlayer.url = path; avPlayer.stateChange = (state: AVPlayerState, reason: Error) => { if (state === AVPlayerState.COMPLETED) { // 通过 EventChannel 推送播放完成事件 } if (state === AVPlayerState.ERROR) { // 把错误信息返回给 Dart 层 } }; await avPlayer.prepare(); await avPlayer.play(); // 启动定时器,定时查询进度并通过 EventChannel 上报 }这里有个非常容易踩的坑:AVPlayer 是有状态机的。create 完之后,播放器处于初始状态,你必须先调用 prepare,等它进入 prepared 状态才能 play。如果你在没有监听状态变化的情况下连续调用 prepare 和 play,轻则播放失败,重则直接抛异常。
正确的理解方式是把它当成一个异步状态机。创建成功之后,prepare 会触发状态流转到 prepared,play 之后从 prepared 流转到 playing。整个过程的每个阶段都可能失败,所以状态监听要尽早挂,最好在 create 之后就挂,这样能捕获到完整的异常链路。
AVPlayer 还支持直接播放网络 URL。也就是说,Dart 层传进来的是 http 或 https 开头,你不需要先下载到本地,直接把 URL 丢给 avPlayer.url 就行。但这就更要求网络权限配置正确,否则会一直卡在准备阶段。
3.3 录音与事件通道的适配思路
flutter_sound 的录音能力走的是另一条链路。在 OpenHarmony 侧,采集 PCM 数据的首选是 AudioCapturer,它负责把麦克风采到的数据往内存里送。如果你还需要压缩成 mp3 或 aac,得再串一个 AudioEncoder。
录音流程比播放要繁琐一些:
- 先申请麦克风权限,而且必须是运行时动态申请,不能在配置文件里写完就完事;
- 根据采样率、声道数、采样位数配置 AudioCapturer;
- 启动采集后,不断从 capturer 里读数据,写入文件或做实时处理;
- 停止录音后,如果目标是 WAV 格式,还要补写 WAV 文件头。
申请麦克风权限的代码片段大概是这样的:
import { abilityAccessCtrl } from '@ohos.abilityAccessCtrl'; const permissionResult = await abilityAccessCtrl.requestPermissionsFromUser( context, ['ohos.permission.MICROPHONE'] );用户拒绝授权的情况下,录音初始化会失败,一定要把这个失败状态映射回 Dart 层,否则 Flutter 侧会一直等不到回调。
给一个实际建议:如果你当前业务只用到播放,不要在适配阶段把录音也牵扯进来。录音权限弹窗会打断自动化测试,录音链路一旦出问题还会影响你排查播放问题。先把播放跑通,再加录音,问题边界越窄越好定位。
3.4 音频资源、缓存与权限处理
资源处理是适配过程中最容易翻车的隐性环节。flutter_sound 的 startPlayer 可能收到这几种值:
- file:// 开头的本地文件路径,直接转给 AVPlayer;
- http/https 网络地址,直接转给 AVPlayer;
- asset:// 开头的 Flutter asset 资源,不能直接给 AVPlayer,因为它需要文件访问能力,而 Flutter asset 并没有暴露一个稳定的文件路径。
asset 资源的常规处理办法,是把 asset 从 Flutter 侧读取出来,写入应用缓存目录,然后再以 file:// 路径交给 AVPlayer。流程虽土,但最可靠。
// Dart 层或平台层处理 asset 时,思路如下 final ByteData data = await rootBundle.load(assetPath); final File temp = File('${Directory.systemTemp.path}/audio_xxx.mp3'); await temp.writeAsBytes(data.buffer.asUint8List()); // 再把 temp.path 传给平台层播放文件用完记得清理。有些实现会把所有临时音频文件都放在缓存目录,长时间跑下来垃圾会越积越多。比较推荐的做法是在 stopPlayer 或播放完成时统一清理当前文件,保留最近一次播放的内容用于 seek 和续播。
4. 构建、验证与能力差异
4.1 在应用工程中引入适配结果
插件侧代码写完后,需要把它接入实际应用工程来验证。如果你用的是本地改动的源码版 flutter_sound,最直接的方式是通过 path 依赖引用:
dependencies: flutter_sound: path: third_party/flutter_sound接着运行工程的构建脚本,构建出可安装到设备上的应用包。构建阶段如果报错,大多数情况是 ohos 模块没有被正确包含进编译链路,优先检查插件目录下的配置文件,确认平台目录是否被识别。
构建成功只是起点,真正的验证要看运行时。建议把下面几项做成检查清单:
- 日志里能看到插件注册成功的标志,不再抛 MissingPluginException;
- 本地文件能播放,播放开始和结束都有回调;
- 网络音频能播放,断网时有错误回调;
- asset 资源能播放,没有权限和路径问题;
- 暂停、恢复、seek、音量调节全部有响应;
- 退出页面后播放停止,再次进入可以重新播放。
4.2 功能支持矩阵与实测观察
适配完成后,我用表格把 flutter_sound 的主要能力和这次适配后的 OpenHarmony 支持情况整理了出来,方便对照:
| 能力项 | 支持情况 | 说明 |
|---|---|---|
| 本地文件播放 | 支持 | 推荐先用 wav/mp3 验证 |
| 网络音频播放 | 支持 | 需要 INTERNET 权限 |
| asset 音频播放 | 支持 | 需要先解压到临时目录 |
| 播放进度上报 | 支持 | 通过 EventChannel 定时推 |
| 暂停/恢复 | 支持 | 对应 AVPlayer 的 pause/play |
| seek 跳转 | 支持 | 注意部分格式 seek 精度一般 |
| 播放完成回调 | 支持 | 监听 AVPlayer COMPLETED 状态 |
| 录音采集 | 条件支持 | 需要动态申请麦克风权限 |
| mp3 播放 | 支持 | 最常用的验证音频 |
| wav 播放 | 支持 | 兼容性好,排查问题优先用 |
格式这块多说一句。OpenHarmony 多媒体服务对常见开放格式支持不错,mp3、wav、aac、flac 这类基本没问题,但某些特定编码的音频,比如特殊采样率的 PCM 封装文件,可能无法直接播放。碰到播放失败的音频,第一反应不要是怀疑代码,先换一个标准 wav 文件测试。如果标准 wav 能播,那就是格式兼容性的问题,不是插件桥接的问题。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
我把适配过程中最容易撞上的问题整理成一个速查表,按“现象 - 可能原因 - 排查方向”的结构来写:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 调用播放方法直接 MissingPluginException | ohos 平台模块没被应用工程包含 | 检查插件平台目录配置和构建日志 |
| startPlayer 一直卡住不返回 | URL 资源不可达 / 播放器没进入 prepared | 换本地文件测试,看 AVPlayer 状态日志 |
| 能播放但没声音 | 音频流类型设置不对 / 系统输出路由问题 | 检查音量,强制设置媒体音乐流 |
| 进度回调时有时无 | EventChannel 注册名和 Dart 层不一致 | 核对两边 channel 名 |
| 播放完成不回调 | 只监听 play 完的状态,没监听 COMPLETED | 补全 stateChange 分支 |
| 录音返回全零数据 | 麦克风权限没授予 / AudioCapturer 参数不匹配 | 动态申请权限,核对采样率和声道数 |
| 页面退出后崩溃 | 播放器没有在 onDetach 里 release | 在 detach 和销毁流程统一释放 |
5.2 排查链路与日志技巧
音频插件的坑,很多都不是一次就能定位的。我给你一套我常用的排查链路。
第一步,从 Dart 侧打印调用参数。在 invokeMethod 附近加一行日志,把方法名、参数原始值打出来。这样能确认问题出在上游还是下游。
第二步,在插件入口的 handleMethodCall 里打日志。如果入口都没打印,说明通道没注册成功,直接查插件注册;如果入口打了但后续没反应,问题出在原生实现。
第三步,用 hilog 抓 OpenHarmony 侧日志。平时开发可能只看 Flutter 日志,但 AVPlayer 的状态变化和底层错误码只有系统侧日志里才有。过滤关键字要带上 AVPlayer、MediaService、AudioCapturer 这些模块名,很快就能看到播放器卡在哪个状态。
第四步,备一个标准音频文件用于交叉验证。我建议你的测试工程里常备一个 1 分钟的 wav 和一个 1 分钟的 mp3,任何播放问题都用这两个文件先跑一遍。wav 能排除解码器问题,mp3 能验证常见压缩格式链路,两个都通过后再去排查业务音频的特殊性。
最后再补一个小技巧:适配过程中,尽量在一开始就把 Dart 层所有调用统一收敛到几个方法里,不要直接散落在各个页面。这样你想排查某个方法出问题,只要改一处测试入口就能覆盖所有调用场景。flutter_sound 这种功能多的库尤其需要这么干,不然你根本分不清是播放器的问题、录音器的问题还是事件通道的问题。