news 2026/10/19 5:02:56

Flutter/OpenHarmony 三方库适配实战:sound_stream 音频流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter/OpenHarmony 三方库适配实战:sound_stream 音频流

Flutter/OpenHarmony 三方库适配实战:sound_stream 音频流

音频流插件的适配难点不在“把麦克风打开”,而在于如何让同一条 Dart
MethodChannel在 OHOS 上继续传递 PCM 数据、回传状态、处理权限,并在页面退出
时释放原生资源。sound_stream原本面向 Android 和 iOS,Dart 层使用
RecorderStream和PlayerStream暴露采集、播放、状态和扬声器切换能力。
本次适配版本为0.4.2-ohos.1,目标是保持这些接口不变,只补齐 OHOS HAR
实现。

一、项目信息与适配目标

项目内容
仓库名称flutter_sound_stream_zh
原始包名sound_stream
适配版本0.4.2-ohos.1
方法通道vn.casperpas.sound_stream:methods
音频格式PCM S16LE、单声道
本地状态Dart 分析、示例测试、debug Hap 构建已通过;虚拟机运行待测

适配链路如下:

RecorderStream / PlayerStream | v MethodChannel("vn.casperpas.sound_stream:methods") | v SoundStreamPlugin (ArkTS) | v AudioCapturer + AudioRenderer | v PCM Uint8List / status events

二、先保住 Dart API,不让业务代码分叉

原库的 Dart 层已经把麦克风数据封装成Stream<Uint8List>,播放端接收同样的
字节流。如果为 OHOS 单独设计一套类名或方法,业务页面就必须写平台判断,音频
数据也会在不同平台产生不同格式。因此本次没有修改RecorderStream和
PlayerStream的公开接口。

例如初始化和采集仍然沿用原调用:

finalrecorder=RecorderStream();finalplayer=PlayerStream();awaitFuture.wait([recorder.initialize(sampleRate:16000),player.initialize(sampleRate:16000),]);recorder.audioStream.listen((Uint8Listchunk){player.writeChunk(chunk);});awaitrecorder.start();awaitplayer.start();

方法通道覆盖的名称也保持不变:

Dart 方法OHOS 原生动作
initializeRecorder申请麦克风权限并创建AudioCapturer
startRecording/stopRecording启动或停止采集循环
initializePlayer创建AudioRenderer
startPlayer/stopPlayer启动或停止渲染
writeChunk校验 PCM 字节并写入渲染器
usePhoneSpeaker通过AudioRoutingManager切换通信设备

三、让 Flutter OHOS 工具链找到插件

只创建ohos/目录并不够,插件根pubspec.yaml必须声明 OHOS 平台入口:

version:0.4.2-ohos.1flutter:plugin:platforms:android:package:vn.casperpas.sound_streampluginClass:SoundStreamPluginios:pluginClass:SoundStreamPluginohos:pluginClass:SoundStreamPlugin

OHOS 模块按普通 HAR 配置:

{ "module": { "name": "sound_stream", "type": "har", "deviceTypes": ["default", "tablet"] } }

ohos/index.ets只负责导出 ArkTS 插件类,避免示例工程直接依赖内部文件:

importSoundStreamPluginfrom'./src/main/ets/components/plugin/SoundStreamPlugin';exportdefaultSoundStreamPlugin;

这三层声明分别回答三个问题:Dart 如何注册平台实现、Hvigor 如何编译 HAR、
示例应用如何加载入口。

四、权限:不能在插件加载时就索取麦克风

插件被 Engine 加载时可能还没有进入录音页面。如果此时立即调用权限接口,
应用会在用户没有发起音频操作时弹窗,也不利于生命周期管理。OHOS 实现将权限
请求放在initializeRecorder中:

privateasynchasMicrophonePermission():Promise<boolean>{constcontext=this.uiAbilityContext;if(context===null){returnfalse;}constpermissions:Permissions[]=[MICROPHONE_PERMISSION];constrequest=awaitabilityAccessCtrl.createAtManager().requestPermissionsFromUser(context,permissions);returnrequest.authResults.length>0&&request.authResults.every((value:number)=>value===0);}

插件实现AbilityAware,在onAttachedToAbility中保存当前 UIAbility
Context,在onDetachedFromAbility中清空引用。权限和音频设备都依赖当前
Ability,不能只保存 ApplicationContext。

示例应用的entry/src/main/module.json5同时声明权限和用途:

{"name":"ohos.permission.MICROPHONE","reason":"$string:microphone_permission_reason","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}}

权限失败通过result.error('permission_denied', ...)返回,Dart 层不会得到
一个虚假的true。

五、采集循环:把 AudioCapturer 变成 Dart 数据流

初始化时统一使用单声道 PCM S16LE:

conststreamInfo:audio.AudioStreamInfo={samplingRate:sampleRate,channels:audio.AudioChannel.CHANNEL_1,sampleFormat:audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,encodingType:audio.AudioEncodingType.ENCODING_TYPE_RAW,};constcapturerInfo:audio.AudioCapturerInfo={source:audio.SourceType.SOURCE_TYPE_MIC,capturerFlags:0,};this.recorder=awaitaudio.createAudioCapturer({streamInfo,capturerInfo});

启动录音后,ArkTS 维持一个异步读取循环。读取到字节就转换为Uint8Array
并通过dataPeriod事件发回 Dart:

while(this.recorderLoopRunning&&recorder===this.recorder&&recorder.state===audio.AudioState.STATE_RUNNING){constbuffer=awaitrecorder.read(RECORD_BUFFER_SIZE,true);if(buffer.byteLength>0){this.sendEvent('dataPeriod',newUint8Array(buffer));}}

这里的三个条件缺一不可:停止标志让循环退出,引用比较避免释放旧对象后继续
读取,状态检查避免已经停止的采集器被反复唤醒。Dart 端只需监听原有流:

_audioStream=_recorder.audioStream.listen((data){_micChunks.add(data);});

六、播放端:保留原始 PCM,不引入文件缓存

播放器初始化同样使用 PCM S16LE 单声道,然后接收 Dart 传来的字节:

constrendererInfo:audio.AudioRendererInfo={usage:audio.StreamUsage.STREAM_USAGE_MUSIC,rendererFlags:0,};this.renderer=awaitaudio.createAudioRenderer({streamInfo,rendererInfo});

writeChunk会先检查参数类型,再切出当前Uint8List对应的缓冲区,避免把
一个已经被 Dart 回收的视图直接交给原生层:

constvalue:ESObject=call.argument('data');letbytes:Uint8Array;if(valueinstanceofUint8Array){bytes=valueasUint8Array;}elseif(valueinstanceofArrayBuffer){bytes=newUint8Array(valueasArrayBuffer);}else{result.error('invalid_argument','data must be a byte array',null);return;}constbuffer=bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength,)asArrayBuffer;awaitrenderer.write(buffer);

播放在startPlayer时启动,在stopPlayer或插件销毁时停止。渲染器和
采集器都在releaseRecorder、releaseRenderer中执行停止和释放,释放失败
只记录为 teardown 过程中的尽力而为,不让页面退出被异常阻塞。

七、扬声器切换与状态回传

usePhoneSpeaker不直接修改 Dart 变量,而是调用 OHOS 通信设备路由:

constrouting=audio.getAudioManager().getRoutingManager();awaitrouting.setCommunicationDevice(audio.CommunicationDeviceType.SPEAKER,value,);

状态事件继续使用原库约定的字符串:

privatesendEvent(name:string,data:Object):void{constpayload:Record<string,Object>={'name':name,'data':data,};this.channel.invokeMethod('platformEvent',payload);}

Dart 端因此可以继续把recorderStatus和playerStatus映射为
SoundStreamStatus.Playing、Stopped等枚举,不需要增加 OHOS 专用状态。

八、示例工程与权限体验

示例应用保留原有的三个主要动作:

  1. 设置按钮初始化录音器和播放器;
  2. 麦克风按钮开始或停止采集;
  3. 播放按钮开始或停止渲染,并支持扬声器切换。

example/test/widget_test.dart改成验证真实界面标题和设置入口,避免模板
计数器测试与当前音频界面不一致。example/test/golden_test.dart使用 Flutter
golden 渲染同一界面,生成本地可复现的界面图:

这张图是本地 Flutter 渲染证据,不是 OHOS 真机截图。虚拟机运行后,应补充麦克风
权限弹窗、录音状态变化、播放状态和扬声器切换的设备截图。

九、验证过程与结果

本次执行的完整命令如下:

flutter pub get flutter analyzecdexample flutter pub get flutter analyze fluttertestflutter build hap--debug--no-codesign

当前环境结果:

检查项结果
根包flutter analyze通过,No issues found
示例flutter analyze通过,No issues found
示例flutter test通过,界面测试和 golden 测试均通过
flutter build hap --debug --no-codesign通过
生成 Hapexample/build/ohos/hap/entry-default-unsigned.hap
虚拟机/真机运行待测试,当前环境无 OHOS 设备

Hvigor 成功只证明 HAR、entry 模块、Dart 产物和权限配置可以组合成 Hap,不
等于麦克风权限、音频设备和路由策略已经在设备上验证。这个边界必须保留。

十、虚拟机测试清单

拿到虚拟机后建议按顺序验证:

  1. 安装entry-default-unsigned.hap,确认应用能够启动。
  2. 点击设置按钮,观察麦克风权限弹窗和初始化结果。
  3. 点击麦克风按钮,确认状态事件变为Playing,日志中出现非空 PCM 数据。
  4. 点击播放按钮,确认AudioRenderer启动并能听到声音。
  5. 切换扬声器按钮,记录设备是否支持通信设备路由切换。
  6. 返回页面或退出应用,确认没有继续读取麦克风、没有重复回调。
  7. 截取权限、录音、播放、扬声器切换和退出后的界面,作为后续回归证据。

如果出现permission_denied,先检查module.json5和权限 reason;如果状态
事件没有变化,检查SoundStreamPlugin是否注册、onAttachedToAbility是否
拿到 UIAbility Context、方法名是否完全一致;如果有状态但没有声音,再检查
采样率、单声道配置和writeChunk的字节类型。

十一、适配总结

这次适配的价值在于把“能编译的 OHOS 文件”推进成一条可追踪的音频链路:

  • Dart API 和方法通道保持原样,业务调用不需要增加 OHOS 分支。
  • 权限在初始化录音时申请,并通过明确的错误码返回失败。
  • AudioCapturer、AudioRenderer和路由接口承担平台能力,数据不落盘。
  • Ability 生命周期负责 Context,插件销毁时负责释放采集器和渲染器。
  • Dart 分析、测试、golden 渲染和 Hap 构建构成当前的自动化验收。
  • 虚拟机运行、真实权限、声音和路由表现单独记录,不与构建成功混为一谈。

后续升级 Flutter OH 或 HarmonyOS SDK 时,应重点回归AudioCapturer.read、
AudioRenderer.write、setCommunicationDevice的签名与权限策略,并保留新的
设备截图。

欢迎加入 CPF-Flutter 鸿蒙社区,一起交流 Flutter 三方库适配、工程实践和
虚拟机/真机验证经验:

https://atomgit.com/CPF-Flutter

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

仅用非门和或非门实现3-10译码器的门级设计与FPGA验证

简介&#xff1a;本资源是面向数字电路与FPGA初学者的Vivado平台组合逻辑设计实践包&#xff0c;聚焦‘仅用非门和或非门实现3-10译码器’这一典型教学任务&#xff0c;帮助学生系统掌握组合电路的设计建模、综合实现与功能验证全流程。压缩包共58个文件&#xff0c;约500KB&am…

作者头像 李华
网站建设 2026/10/19 5:02:42

DID拼接屏软件从选型到部署:架构、调参与自动化运维实战

简介&#xff1a;DID拼接屏软件是一套面向商业展示、监控中心与会议厅等场景的液晶拼接屏控制管理工具&#xff0c;支持屏幕拼接、信号切换、图像校正、分屏显示和远程集中管理等核心功能&#xff0c;帮助用户灵活定制大屏显示方案。压缩包共48个文件&#xff0c;以dll动态库、…

作者头像 李华
网站建设 2026/10/19 5:02:32

图像恢复中的逆滤波器与维纳滤波器:原理、实现与参数调优

1. 图像恢复问题与核心设计思路1.1 为什么图像恢复不是“一键锐化”做图像处理的人迟早会碰上一件糟心事&#xff1a;明明对焦没问题、曝光也正常&#xff0c;拍出来的图却像隔着一层雾&#xff0c;纹理细节全丢&#xff0c;放大看还有密密麻麻的噪点。最开始我惯用套路是锐化掩…

作者头像 李华
网站建设 2026/10/19 5:02:14

构建沟通驱动型CRM:从数据模型到自动化实践

1. 为什么我会自己搭一套"以沟通为中心"的CRM&#xff0c;而不是直接买现成的先说结论&#xff1a;DeskcommCRM不是我在网上买到的某个商业产品&#xff0c;而是我为自己团队业务场景定制的一套客户关系管理系统的代号。这个名字拆开来看就是Desk Communication CRM…

作者头像 李华
网站建设 2026/10/19 5:01:40

open-code-review:基于CLI+LLM Agent的Git Diff精准代码评审工作流

1. 项目概述&#xff1a;这不是一个“工具”&#xff0c;而是一套可落地的开源代码评审工作流“open-code-review”这个名称乍看像某个 GitHub 仓库名&#xff0c;但实际它代表的是一种正在快速演进的工程实践范式——把传统依赖人工、会议、PR评论的代码评审&#xff08;Code …

作者头像 李华