更新记录
1.0.240905(2024-09-09)
- 新增appNativePlugin_sampleRate原生插件录音选项,允许设置录音采样率
- writeFile接口新增seekOffset选项,支持在文件指定位置覆盖写入数据
- iOS新增ios_categoryResetPlayback_AEC原生插件录音选项,用于提高部分音频设备的兼容性
- iOS重写打开录音逻辑,解决老版本打开录音小概率卡住的问题
1.0.240705(2024-07-05)
新增ios_categoryOptions原生插件录音选项,AVAudioSession的CategoryOptions默认值为 0x1|0x4 (MixWithOthers | AllowBluetooth),允许录音时其他app保持播放
1.0.240409(2024-04-10)
第一次发布
查看更多平台兼容性
Android | iOS |
---|---|
× | 适用版本区间:9 - 17 |
原生插件通用使用流程:
- 购买插件,选择该插件绑定的项目。
- 在HBuilderX里找到项目,在manifest的app原生插件配置中勾选模块,如需要填写参数则参考插件作者的文档添加。
- 根据插件作者的提供的文档开发代码,在代码中引用插件,调用插件功能。
- 打包自定义基座,选择插件,得到自定义基座,然后运行时选择自定义基座,进行log输出测试。
- 开发完毕后正式云打包
付费原生插件目前不支持离线打包。
Android 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline_package/android
iOS 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline_package/ios
注意事项:使用HBuilderX2.7.14以下版本,如果同一插件且同一appid下购买并绑定了多个包名,提交云打包界面提示包名绑定不一致时,需要在HBuilderX项目中manifest.json->“App原生插件配置”->”云端插件“列表中删除该插件重新选择
⠀
⠀
Recorder原生录音插件:实时帧回调、文件流式读写
本原生插件提供Android、iOS双端支持(可单独购买),是前端组件 Recorder-UniCore 的配套插件,搭配使用时可以提供丰富的功能支持;或者直接调用本原生插件接口,不过能做到的功能有限。
推荐搭配Recorder-UniCore
前端组件一起使用,可做到:
- 支持已有的大部分录音格式:mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理,包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;注意:不支持通话时录音
- 支持使用部分线控耳机、蓝牙耳机、USB外设的麦克风录音
- 支持文件流式读写,录音数据可以实时写入到文件
- 支持离线使用,前端组件和本原生插件均不依赖网络
详细文档(含Demo项目): https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp
Recorder开源库地址: https://github.com/xiangyuecn/Recorder
如果github打不开,可以点此访问Gitee仓库地址 。
⠀
选择适用你的版本
本原生插件支持Android、iOS分开单独到DCloud插件市场购买,均包含了Recorder-UniCore前端组件的商用授权;你可以只购买授权,费用为 ¥199元 ,相当于同时赠送了Android版原生插件(如果用不着这个原生插件可以不使用即可)。
版本 | 价格 | 说明 |
---|---|---|
授权+送Android | ¥199 | 建议只购买授权、或只Android上使用时购买 |
单iOS+补差价 | ¥399 | 只含iOS版原生插件,建议只iOS上使用时购买 |
Android+iOS | ¥598 | 含Android、iOS原生插件 |
购买后可联系客服,同时提供订单信息,客服拉你进入VIP支持QQ群,在群文件中可下载Recorder-UniCore前端组件的app-uni-support.js
文件最新源码;客服联系方式:QQ(
125
165
4593
)
,或者直接联系作者QQ(
753
610
399
)
(回复可能没有客服及时)。
注:VIP支持群的主要作用是代表你已获得授权许可,可以随时获得
app-uni-support.js
文件最新版源码;不作为问答或售后群使用,当然如果你有问题也可以直接群里问,花费时间不多的,作者免费顺带就解答了,如果复杂花费比较久时间的,可能要适当收点人工费用,或者选择进行付费指导。
Recorder-UniCore
组件中自带的app-uni-support.js
文件是压缩版,功能和源码版一致,在VIP支持群中下载得到此文件源码后,可以直接替换组件中的这个文件,也可以不替换。
⠀
如何开票
插件市场付费购买插件支持开票(由DCloud开具),具体请参考 DCloud 云服务如何开具发票 ;在插件市场 我的插件
- 我使用的插件
- 购买的插件
可查看到购买的插件 和 开票相关指引。
⠀
测试方法
测试时无需购买插件,试用是免费的,并且原生插件试用是无限制的。
- 先到Recorder-UniCore下载示例项目(需根目录执行
npm install
完成recorder-core
依赖的安装),在HBuilder中打开后,在项目manifest.json配置中分配一个uni-app应用标识 - 然后点击本插件页面中的试用按钮,在这个应用标识对应的项目中试用
- 然后在项目manifest.json的App原生插件配置中勾选本原生插件(注意:勾选了试用版时,购买后需要重新勾选成正式版)
- HBuilder中提交云打包打自定义基座
- 连接手机进行调试,使用自定义基座运行即可测试
⠀
录音权限配置
在uni-app项目的 manifest.json
中配置好Android和iOS的录音权限声明。
//Android需要勾选的权限,第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音,需要启用后台录音保活服务,请参考下面的`androidNotifyService`,否则锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音(renderjs中H5录音也受影响)
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置,不然App切到后台后立马会停止录音
⠀
⠀
⠀
使用方法一:搭配Recorder-UniCore前端组件使用
插件集成
- 到插件市场下载安装Recorder-UniCore前端组件,并且项目根目录安装
recorder-core
:npm install recorder-core --registry=https://registry.npmmirror.com/
- 试用或购买本原生插件,在项目manifest.json的App原生插件配置中勾选本原生插件(注意:勾选了试用版时,购买后需要重新勾选成正式版)
- 参考上面的录音权限配置,在项目manifest.json中配置好权限
- 云打包自定义基座进行测试、或正式打包,即完成集成
⠀
调用录音
/** 集成好Recorder-UniCore组件后,直接调用RecordApp相关方法进行录音即可,详细请参考下面的demo_UniApp文档链接
RecordApp.RequestPermission(...) 请求录音权限
RecordApp.Start({ 开始录音
type:"mp3",sampleRate:16000,bitRate:16
//android_audioSource、ios_categoryOptions ... 原生插件支持的更多配置参数,请参考下面recordStart原生接口
}, ...)
RecordApp.Stop(...) 停止录音
RecordApp.Pause() 暂停
RecordApp.Resume() 继续
**/
//在调用RecordApp.RequestPermission之前进行配置,建议放到import后面直接配置(全局生效)
//也可以判断一下只在iOS上或Android上启用,不判断就都启用,比如判断iOS:RecordApp.UniIsApp()==2
RecordApp.UniNativeUtsPlugin={ nativePlugin:true };
//只需进行这个配置就行,在RecordApp录音时就会自动使用原生插件来录音;未启用原生插件时App内默认是在renderjs使用H5录音
//提升后台录音的稳定性:配置了原生插件后,可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码(WebWorker加速),变成逻辑层中直接编码(但会降低逻辑层性能),后台运行时可避免部分手机WebView运行受限的影响
//提升后台录音的稳定性:需要启用后台录音保活服务(iOS不需要),详细请参考下面的`androidNotifyService`
⠀
手动调用原生插件接口
/**App中集成了原生插件后按下面代码进行调用,注意需要先配置RecordApp.UniNativeUtsPlugin
参数:action 字符串,要调用的功能;args 对象,调用参数
返回:any 根据功能定义返回对应的结果,出错会抛异常
比如将任意数据保存到文件:**/
try{
var result=await RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"test.txt", dataBase64:"dGVzdDEyMw=="});
}catch(e){
console.error(e)
}
//更多可用接口请参考下面的《直接调用原生插件接口 - 可用的接口》
⠀
详细文档、RecordApp方法、属性文档
请先阅读 demo_UniApp文档,含Demo项目;更高级使用还需深入阅读 Recorder文档、RecordApp文档 (均为完整的一个README.md文档),Recorder文档中包含了更丰富的示例代码:基础录音、实时处理、格式转码、音频分析、音频混音、音频生成 等等,大部分能在uniapp中直接使用。
⠀
⠀
⠀
使用方法二:直接调用原生插件接口
注意:仅使用原生插件接口来进行录音时,只支持返回pcm格式音频数据,不支持其他格式;pcm数据可以使用Recorder库进行转码成其他格式,或者直接使用Recorder-UniCore前端组件支持更多格式录音。
插件集成
- 试用或购买本原生插件,在项目manifest.json的App原生插件配置中勾选本原生插件(注意:勾选了试用版时,购买后需要重新勾选成正式版)
- 参考上面的录音权限配置,在项目manifest.json中配置好权限
- 云打包自定义基座进行测试、或正式打包,即完成集成
⠀
调用插件接口
//加载插件,注意:如果你使用的单独的Android或iOS版,请使用下面带后缀的
var RecNP=uni.requireNativePlugin("Recorder-NativePlugin");
//var RecNP=uni.requireNativePlugin("Recorder-NativePlugin-Android");
//var RecNP=uni.requireNativePlugin("Recorder-NativePlugin-iOS");
//调用插件接口,固定使用request方法进行调用
RecNP.request({
action:"writeFile" //接口名称,这个例子是将任意数据保存到文件
,args:{path:"test.txt", dataBase64:"dGVzdDEyMw=="} //接口参数
},function(data){ //接口调用结果回调
if(data.status!="success"){
console.error(data.message); //调用错误处理
return;
};
console.log(data.value); //接口调用结果
});
//绑定原生层回调
RecNP.request({action:"jsCall",args:{}},function(data){
if(data.action=="onLog"){ //原生层日志输出
var msg="[RecNP]["+data.tag+"]"+data.message;
data.isError? console.error(msg) : console.log(msg);
}else if(data.action=="onRecord"){ //录音pcm数据回调
var sampleRate=data.sampleRate; //采样率
var pcm=new Int16Array(uni.base64ToArrayBuffer(data.pcmDataBase64)); //16位pcm
console.log("onRecord "+sampleRate+" "+pcm.length);
}
});
//建议自行封装一个函数返回Promise来异步调用,方便好使
var RecNP_CallAsync=function(action,args){ return new Promise(function(resolve, reject){
RecNP.request({ action:action,args:args||{} },function(data){
if(data.status!="success"){
reject(new Error(data.message));
return;
};
resolve(data.value);
});
}) };
//调用,这个例子是将任意数据保存到文件
try{
var result=await RecNP_CallAsync("writeFile",{path:"test.txt", dataBase64:"dGVzdDEyMw=="});
}catch(e){
console.error(e)
}
⠀
可用的接口
【文档说明】
下面的action中,开头的名字为RecNP.request的action,“参数”为args,“返回”为接口的返回值data.value
【录音相关action】 注意:在使用Recorder-UniCore组件时会自动调用这些接口,请勿自行调用
jsCall 【这是一个特殊方法】绑定原生层js回调,当原生层需要给js发送消息时,会进行回调(RecNP.request的回调方法会被反复调用)
参数:{}
返回:{ action:"", ... } //原生层会反复调用回调方法,通过其中的action来判断返回的是什么内容
{action:"noop"} //无需处理,多次调用jsCall时原生层释放老的回调
{action:"onLog",isError:false,tag:"xx",message:"xxx"} //原生层日志输出
{action:"onRecord",sampleRate:44100,pcmDataBase64:"base64"} //录音数据回调,pcm为16位单声道,sampleRate是pcm的采样率
recordPermission 请求录音权限
参数:{}
返回:1 //权限状态code数值:1有权限,3用户拒绝。(2未用到;此接口早期移植的时候忘记改成对象,保留数值格式)
recordStart 开始录音,录音pcm数据会通过jsCall回调,开始后必须每5秒调用一次recordAlive;本方法会获取录音权限,但建议先调用recordPermission提前获取录音权限
参数:{
appNativePlugin_sampleRate:44100 //可选录制的采样率(24/09/05新增),默认返回44100采样率的pcm数据,可取值48000(在js中使用Recorder.SampleData函数来转换成需要的任意采样率);其他采样率值不一定可用,可能导致无法打开录音,请测试好后再提供
,appNativePlugin_AEC_Enable:false //可选是否启用回声消除,默认不启用
,android_audioSource:0 //可选Android指定麦克风源 MediaRecorder.AudioSource,默认值为0,0 DEFAULT 默认音频源,1 MIC 主麦克风,5 CAMCORDER 相机方向的麦,6 VOICE_RECOGNITION 语音识别,7 VOICE_COMMUNICATION 语音通信(带回声消除)。配置值除7外,会禁用回声消除
,ios_categoryOptions:0x1|0x4 //可选iOS的AVAudioSession setCategory的withOptions参数值,取值请下面的iosSetDefault_categoryOptions;默认值为5(0x1|0x4)
,ios_categoryResetPlayback_AEC:false //可选iOS打开录音时重置录音环境,用于提高部分音频设备的兼容性(24/09/05新增);iOS录音开启回声消除时,本原生插件可能无法感知到新插入的USB音频输入设备(如Lightning口/USB领夹麦克风),首次录音前插入的无此影响;可在录音时提供本true启用音频环境重置(默认false不重置),将能感知到所有音频输入设备,但会导致Start变慢;注意:录音中途新插入的USB设备可能任何配置下均无法感知到,需下次Start时才可以
}
返回:{} //空对象
recordStop 停止录音
参数:{}
返回:{} //空对象
recordAlive 定时心跳(开始录音后5秒发一次),如果超过10秒未发心跳,将会停止录音,防止未stop导致泄露
参数:{}
返回:{} //空对象
【其他可用action】
getInfo 获取插件信息
参数:{}
返回:{
info:"" //插件信息字符串
,bundleId:"xx.xx.xx" //当前App包名,Android为Package值、iOS为Bundle ID值
,bundleIds:["allow all"] //插件打包包名列表,默认匹配任何包名
,distInfo:"standard" //插件打包信息,默认standard
}
debugInfo 获取调试信息 (24/09/05新增)
参数:{}
返回:{ appMemoryUsage:123 } //app内存占用大小(不一定准),单位字节;数据来源:Android Debug.getMemoryInfo.TotalPss,iOS task_info.TASK_VM_INFO.phys
setSpeakerOff 切换扬声器外放和听筒播放,随时都可以调用;但需注意打开录音时可能会自动切换播放方式(24/09/05起打开录音默认off:false+headset:true),可在打开录音后调用一次切换成你需要的播放方式。iOS Bug:部分iOS系统版本首次切换或首次打开录音时,可能会导致已有的音频播放暂停但有播放进度变成假的无声,建议切换后或开始录音后再打开播放
参数:{
off:true //必填,true听筒播放,false扬声器播放,连接耳机时此配置无效
headset:true //选填,默认true耳机播放,false扬声器播放(同时使用手机上的麦克风),连接耳机时此配置生效
//配置场景:当由代码进行主动调用,比如开启回声消除录音时想播放的声音大点,就只提供off:false,这时没连接耳机会从扬声器播放,有耳机就从耳机播放
//配置场景:当类似由用户点击外放按钮时调用,同时提供off:false+headset:false,这时不管有没有耳机,都会从扬声器播放
}
返回:{ } //空对象
iosSetDefault_categoryOptions iOS设置默认值,Android不可调用,为iOS的AVAudioSession setCategory的withOptions参数值;recordStart开始录音时如果未提供ios_categoryOptions参数,将会使用此默认值,提供了时将赋值给此默认值;setSpeakerOff调用时也会使用到此默认值
参数:{
value:0x1|0x4 //必填,取值(多选,默认值5=0x1|0x4):0 什么也不设置,0x1 MixWithOthers,0x2 DuckOthers,0x4 AllowBluetooth,0x8 DefaultToSpeaker(不可用,通过setSpeakerOff来切换),0x11 InterruptSpokenAudioAndMixWithOthers,0x20 AllowBluetoothA2DP,0x40 AllowAirPlay,0x80 OverrideMutedMicrophoneInterruption
}
返回:{ } //空对象
writeFile 数据写入文件,可新建文件、追加写入(文件流写入)
参数:{
path:"文件路径" //必填,支持的路径请参考下面
,dataBase64:"base64" //必填,写入的任意内容base64编码,可以为空字符串(如仅新建文件)
,append:false //可选,是否追加写入到文件结尾,默认false会新建文件并写入数据
,seekOffset:-1 //可选,从现有文件指定位置写入并覆盖对应内容(24/09/05新增),取值:-1不指定(默认),0 到 文件长度(0为文件开头位置,append配置无效)
}
返回:{
fullPath:"/文件绝对路径"
}
readFile 读取文件,可流式读取
参数:{
path:"文件路径" //必填,支持的路径请参考下面
,type:"base64" //可选,返回结果类型,默认base64,设为text时将读取成utf-8文本,提供了chunkSize时只支持base64
,chunkSize:0 //可选,本次读取的最大长度,单位字节,默认0读取全部
,chunkOffset:0 //可选,提供了chunkSize时,指定读取的开始位置
}
返回:{
data:"文本或base64" //文件内容,类型取决于提供的type
,isExists:true //文件是否存在;文件不存在时不会返回错误,此时的data为空字符串
,totalSize:0 //文件大小
,fullPath:"/文件绝对路径"
}
deleteFile 删除文件或文件夹
参数:{
path:"文件路径" //必填,支持的路径请参考下面(文件不存在时不会报错)
,isDir:false //可选,true时此路径是文件夹,删除此文件夹,默认false
}
返回:{ fullPath:"/文件绝对路径" }
moveFile 移动或重命名文件
参数:{
fromPath:"源文件路径" //必填,支持的路径请参考下面
,path:"新文件路径" //必填,如果存在会覆盖
}
返回:{ fullPath:"/移动后的文件绝对路径" }
copyFile 复制文件
参数:{
fromPath:"源文件路径" //必填,支持的路径请参考下面
,path:"新文件路径" //必填,如果存在会覆盖
}
返回:{ fullPath:"/复制后的文件绝对路径" }
resolvePath 解析路径成绝对路径
参数:{
path:"文件或文件夹路径" //必填(空字符串时为store目录),支持的路径请参考下面
,pathInfo:false //可选,是否返回路径信息,默认false不返回
}
返回:{
fullPath:"/文件绝对路径"
,pathInfo:{ //可选返回路径信息
isExists:true //文件或文件夹是否存在
,isFile:true //true时path为文件,false时为文件夹
,size:123 //isFile时文件大小,文件夹为0
,date:123456 //isFile时文件更新时间,毫秒,文件夹为0
}
}
listPath 读取文件夹内的文件
参数:{ path:"文件夹路径" } //必填(空字符串时为store目录),支持的路径请参考下面
返回:{
files:[ { //此文件夹下的文件
name:"文件名"
,size:123 //文件大小
,date:123456 //文件更新时间,毫秒
} ]
,dirs:[ "文件夹名" ] //此文件夹下的文件夹
,fullPath:"/文件夹绝对路径" //结尾不带/
}
androidNotifyService 搭配常驻通知的Android后台录音保活服务(24/09/05新增),iOS不可调用。注意:需要在项目根目录提供AndroidManifest.xml配置才可调用本接口,调用时App必须在前台(适配Android 12+),需要先调用请求录音权限后才能开启服务(适配Android 14+),详细请参考下面的“Android后台录音保活”
参数:{
title:"录音通知标题" //打开服务时必填,close为true时无需提供
,content:"录音通知内容" //可选,close为true时无需提供
,close:false //设为true时关闭服务和通知,不管打开服务是否成功,都需要调用close关闭
}
返回:{ } //close时返回空对象
返回:{ //非close时只要成功返回结果,就代表服务已在运行,但通知可能不显示或会延迟显示,并不影响服务运行
notifyPermissionCode:1 //仅供参考,通知的显示权限状态,取值:1有通知权限,3不确定,当App的targetSdkVersion>=33(Android 13+)时代表无通知权限,小于33时可能系统设置中未打开App的通知或正在弹框询问是否允许通知
,notifyPermissionMsg:"" //仅供参考,权限code对应的消息,当App的targetSdkVersion>=33时为:`(已|未)获得App的通知权限`,小于33时为:`系统设置中App的通知(已|未)打开`
}
androidStoragePermission__limited 简易获取Android的外部存储权限,iOS不可调用,当你需要读写当前应用数据以外的文件时(如手机的Download目录文件),需要先获取外部存储权限;注意这个只会请求WRITE_EXTERNAL_STORAGE权限,因此TargetSDK需小于33(Android 13),否则此权限永远是拒绝的(请自行用别的途径获取权限)
参数:{}
返回:{ code:1 } //权限状态:1有权限,3用户拒绝。(2未用到)
【支持的路径】
"store://文件夹/文件.png" 或 "文件夹/文件.png" (开头不带/)
app内部保存文件,文件夹是可选的
Android为app的file目录 + 文件夹/文件.png
iOS为app的Library/Files目录 + 文件夹/文件.png
"__doc://文件夹/文件.png"
app内部保存文件,文件夹是可选的;兼容iOS Documents目录专用的,正常用"store://"就够了
Android为app的file/__doc目录 + 文件夹/文件.png
iOS为app的Documents目录 + 文件夹/文件.png
"cache://文件夹/文件.png"
app内部缓存文件,文件夹是可选的,存储的文件可能会被用户或系统删除
Android为app的cache目录 + 文件夹/文件.png
iOS为app的Library/Caches目录 + 文件夹/文件.png
"file:///绝对路径/文件.png" 或 "/绝对路径/文件.png" (开头有/)
绝对路径,一般只允许读写app自己目录内的文件;Android获取到了外部存储权限时(调用androidStoragePermission__limited),可能可以可读写外部存储中的文件;iOS不支持读写非app自己目录文件
⠀
Android后台录音保活
自Android 9
开始,为了保护用户隐私,锁屏或进入后台一段时间后App可能会被禁止访问麦克风、摄像头等功能,导致无法录音、或录音数据全部是静音,因此需要使用保活机制才能在后台录音;H5录音、原生接口录音均受影响。iOS不存在此类问题,iOS只要在manifest.json的后台运行能力
中提供audio
配置,即可在后台录音。
本插件已提供androidNotifyService接口,来控制搭配常驻通知的Android后台录音保活服务,打开服务后可提升App在后台或锁屏后录音的稳定性,注意:不同机型的录音保活效果不一定100%有效;此服务为前台服务(Foreground services),在启动的时候会在状态栏显示一个常驻的通知(通知可能不显示或会延迟显示,并不影响服务运行)。
【注意】targetSdkVersion>=31(Android 12+
)时,App必须在前台才能调用此服务接口,在后台时不允许调用;targetSdkVersion>=34(Android 14+
)时,microphone
类型的服务依赖录音权限,因此建议先请求录音权限(调用RecordApp.RequestPermission、或recordPermission接口),成功后再来调用androidNotifyService接口。
【注意】使用前台服务后,在上架时可能需要声明需要使用的任何前台服务类型、提供使用说明和具体用例,否则可能导致部分应用市场无法上架;为了不影响不使用此服务的App,要使用本服务前要在AndroidManifest.xml中进行配置后才能使用。
//请在项目根目录新建AndroidManifest.xml文件,填入以下内容(修改后需要重新打包才会生效)
//文档篇幅有限,下面内容的原文在demo项目的同名文件中,可在demo中找到此文件,或到github查看:https://github.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="io.dcloud.nativeresouce">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<application>
<service
android:name="uni.plugin.recorder.RecorderNativePlugin$RecNotifyService"
android:foregroundServiceType="microphone"
android:exported="false">
</service>
</application>
</manifest>
//上面这段代码仅用于Android后台录音保活服务,如果项目不需要后台录音保活,可以不复制
//iOS只要在manifest.json的`后台运行能力`中提供`audio`配置,即可在后台录音
⠀
⠀