- 示例工程
- 前端
- 移动开发
- 跨平台
【免费下载链接】uni-app
A cross-platform framework using Vue.js
本篇技术指南以 uni-app / uni-app x 官方 API 文档 get-recorder-manager.md 为骨架,结合开源仓库中 uni-recorder 模块的 Android、iOS、HarmonyOS 三端真实实现,系统讲解uni.getRecorderManager()录音管理的返回值结构、RecorderManager全部方法与事件、start()参数语义、采样率与码率匹配关系、错误码含义,并给出可直接运行的完整 uvue 示例。读完本文,你将能够在 uni-app x 的 App(Android/iOS/HarmonyOS)与微信小程序端独立实现"开始—暂停—继续—停止—播放"的完整录音功能链路,并能根据错误码定位权限、参数与系统打断类问题。
一、API 概览与平台兼容性
uni.getRecorderManager()用于获取全局唯一的录音管理对象(RecorderManager),它是 uni-app x 中媒体(media)模块的核心录音 API,适用于语音消息、语音笔记、语音输入、会议记录等需要录制并回放音频的场景。
在仓库源码中,该 API 的协议名称定义于 protocol.uts:
export const API_GET_RECORDER_MANAGER = 'getRecorderManager'接口签名定义于 interface.uts:
getRecorderManager() : RecorderManager export type GetRecorderManager = () => RecorderManager;平台兼容性
| Web | 微信小程序 | Android | iOS | HarmonyOS | | :- | :- | :- | :- | :- | | x | 4.41 | 4.61 | 4.61 | 4.61 |
其中 "x" 表示该平台不支持。Web 平台不支持此 API,需要通过插件市场中的三方录音插件拉齐能力;微信小程序自 4.41 版本、App 三端(Android/iOS/HarmonyOS)自 4.61 版本(HarmonyOS 侧 uniVer 为 4.31 起)开始支持。需要说明的是,uni-app x 环境中 App 平台的能力由 UTS 插件实现,文档兼容性表中的unixVer(uni-app x 编译器/HBuilderX 版本)与unixUtsPlugin(UTS 插件版本)是实际生效的版本号。
注意:该 API 不支持 Web,请将示例运行到 App 平台或微信小程序体验。Web 端如需录音能力,可自行在插件市场检索
getRecorderManager相关插件。
二、返回值:RecorderManager 及其单例语义
| 类型 | | :- | | RecorderManager |
RecorderManager是录音管理器的接口类型,负责录音生命周期控制与事件分发。从源码实现看,各平台均采用单例模式:
- Android 端(app-android/index.uts):
recordManager变量为空时new RecorderManagerImpl(),否则复用; - HarmonyOS 端(app-harmony/index.uts):同样以模块级变量缓存实例,并通过
defineSyncApi注册为同步 API; - iOS 端(app-ios/index.uts):直接返回
UniAudioRecorderManager.shared共享实例。
这意味着在应用生命周期内,多次调用uni.getRecorderManager()拿到的是同一个录音管理器,事件监听与录音状态在全局共享,使用时应注意在页面卸载时注销监听并停止录音(详见示例代码)。
三、RecorderManager 方法全览
RecorderManager接口在 interface.uts 中定义,方法可分为控制类与事件类两组。
3.1 控制类方法
start(options : RecorderManagerStartOptions) : void —— 开始录音
开始录音是使用频率最高、参数最复杂的方法,其options参数类型为RecorderManagerStartOptions。兼容性:微信小程序 4.41、Android 4.61、iOS 4.61、HarmonyOS 4.61、Web 不支持。
pause() : void —— 暂停录音
暂停录音。App-Android 平台仅在 Android 7.0 及以后版本支持,低版本 Android 上调用无效。兼容性:微信小程序 4.41、Android(系统 7.0+)4.61、iOS 4.61、HarmonyOS 4.61。
resume() : void —— 继续录音
恢复被暂停的录音,同样受 Android 7.0 系统版本限制。兼容性:微信小程序 4.41、Android(系统 7.0+)4.61、iOS 4.61、HarmonyOS 4.61。
stop() : void —— 停止录音
停止录音并触发onStop回调返回录音文件临时路径。兼容性:微信小程序 4.41、Android 4.61、iOS 4.61、HarmonyOS 4.61。
3.2 事件类方法
事件方法遵循"onXxx注册监听、offXxx取消监听"的成对模式。注意:offXxx系列在App 端(Android/iOS/HarmonyOS)需要 5.0 版本才支持,微信小程序端 4.41 即支持,Web 均不支持。
| 方法 | 说明 | 兼容性(on / off) | | :- | :- | :- | | onStart / offStart | 录音开始事件 / 取消监听 | 4.41 / 微信;4.61 / Android·iOS·HarmonyOS;off 系列 App 需 5.0 | | onPause / offPause | 录音暂停事件(Android 需 7.0+) | 同上;off 系列 App 需 5.0 | | onResume / offResume | 录音继续事件(Android 需 7.0+) | 同上;off 系列 App 需 5.0 | | onStop / offStop | 录音停止事件,回调文件地址 | 同上;off 系列 App 需 5.0 | | onFrameRecorded / offFrameRecorded | 已录制完指定帧大小的分片文件回调(仅微信小程序支持,且需设置 frameSize,仅 mp3) | 微信 4.41;App 三端均 x | | onError / offError | 录音错误事件,回调错误信息 | 同上;off 系列 App 需 5.0 | | onInterruptionBegin / offInterruptionBegin | 录音因受系统占用而被中断开始事件 | 微信 4.41;App 4.61;off 系列 App 需 5.0 | | onInterruptionEnd / offInterruptionEnd | 录音中断结束事件 | 微信 4.41;App 4.61;off 系列 App 需 5.0 |
四、start 参数详解:RecorderManagerStartOptions
start()的options参数是录音配置的核心,下表完整列出各字段(兼容性列中的版本号与x含义同上文):
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 | | :- | :- | :- | :- | :-: | :- | | duration | number | 否 | 60000 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 指定录音时长,单位 ms。传入合法 duration 后,到达时长会自动停止录音。默认值 60000(1 分钟);微信小程序最大值 600000(10 分钟),App 平台没有最大值限制| | sampleRate | number | 否 | 平台相关 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 采样率,有效值 8000 / 16000 / 44100。Android 平台默认 8000,iOS 平台默认 44100| | numberOfChannels | number | 否 | 2 | Web: x;微信 4.41;Android 4.61;**iOS: x**;HarmonyOS 4.61 | 录音通道数,有效值 1 / 2。注意 iOS 平台不支持此参数配置 | | encodeBitRate | number | 否 | 48000 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 编码码率,有效值范围与采样率强相关(见下文表格) | | format | string | 否 | aac | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 音频格式,合法值见下表 | | frameSize | number | 否 | — | Web: x;微信 4.41;Android/iOS/HarmonyOS 均 x| 指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容会回调录制的文件内容(触发 onFrameRecorded),不指定则不会回调。暂仅支持 mp3 格式,且仅微信小程序端有效|
format 合法值
| 合法值 | 兼容性 | 描述 | | :- | :-: | :- | | aac | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | aac 格式(默认) | | mp3 | Web: x;微信 4.41;Android 4.61;iOS: x;HarmonyOS 4.61 | mp3 格式 | | pcm | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | pcm 格式 | | wav | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | wav 格式 | | m4a | Web: x;微信 4.41;Android: x;iOS: x;HarmonyOS 4.61 | m4a 格式(HarmonyOS 特有) |
源码中的参数默认值处理
从 Android 平台实现(app-android/index.uts)可以看到参数默认值的真实落地逻辑,与文档描述一致:
innerStart(options : RecorderManagerStartOptions) : void { if (options == null) options = {} as RecorderManagerStartOptions if (options.format == null) options.format = DEFAULT_FORMAT // 'aac' if (options.duration == null) options.duration = DEFAULT_DURATION // 1000 * 60 if (options.duration! <= 0) options.duration = DEFAULT_DURATION // 非法时长回退默认值 if (options.sampleRate == null) options.sampleRate = 8000 if (options.numberOfChannels == null) options.numberOfChannels = 2 if (options.encodeBitRate == null) options.encodeBitRate = 48000 this.manager.setAudioParams(options.sampleRate!.toInt(), options.numberOfChannels!.toInt(), options.encodeBitRate!.toInt()) this.manager.setSaveToFile(true) this.manager.setMaxRecordingDuration(options.duration!.toLong()) this.manager.startRecording(format, createFile(options.format! as string)) }其中format会被映射为平台录音器的RecordingFormat枚举(AAC/MP3/PCM/WAV),未知格式回退到 AAC;录音文件输出路径由 app-android/utils.uts 的createFile()生成,存放在应用缓存目录的uni-recorder/子目录下,文件名以时间戳命名、扩展名跟随所选格式:
export function createFile( format : string) : string { let cacheDir = new File(UTSAndroid.getAppCachePath(), "uni-recorder/") if (!cacheDir.exists()) { cacheDir.mkdirs() } const fileName = `${Date.now()}.${format.toLocaleLowerCase()}` return cacheDir.path+'/'+fileName }从该实现可以推断:录音停止后onStop回调中的tempFilePath即为上述缓存目录下的真实文件路径,开发者可将其用于后续播放(uni.createInnerAudioContext())或上传。
五、采样率与编码码率匹配关系(重要)
sampleRate与encodeBitRate必须匹配,否则将触发错误码1107603(采样率和编码码率不匹配)。原文档给出了如下权威匹配表。
Android、iOS、微信小程序
| 采样率 | 编码码率 | | :- | :- | | 8000 | 16000 - 48000 | | 11025 | 16000 - 48000 | | 12000 | 24000 - 64000 | | 16000 | 24000 - 96000 | | 22050 | 32000 - 128000 | | 24000 | 32000 - 128000 | | 32000 | 48000 - 192000 | | 44100 | 64000 - 320000 | | 48000 | 64000 - 320000 |
HarmonyOS 平台特殊规则
- aac 编码格式支持码率范围
[32000 - 500000] - mp3 编码格式支持码率范围
[8000, 16000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000]- 采样率使用 16K 以下时,对应码率范围为
[8000 - 64000] - 采样率使用 16K ~ 32K 时,对应码率范围为
[8000 - 160000] - 采样率使用 32K 以上时,对应码率范围为
[32000 - 320000]
- 采样率使用 16K 以下时,对应码率范围为
- wav 编码格式时,固定补丁码率 8000、采样率 64000、通道数 1
实战建议:若不确定码率如何取值,可直接不传
encodeBitRate与sampleRate,让其使用平台默认值(Android 8000 / iOS 44100,码率 48000),即可规避匹配类错误。
六、事件回调的结果对象与错误码
6.1 onStop 回调:RecorderManagerOnStopResult
录音停止事件会回调文件地址,其结果对象定义于 interface.uts:
| 名称 | 类型 | 必备 | 兼容性 | 描述 | | :- | :- | :- | :-: | :- | | tempFilePath | string | 是 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 录音文件的临时路径 |
Android 平台实现中该对象由RecorderManagerOnStopResultCallback承载,tempFilePath直接取录音器输出路径(app-android/index.uts):
override onRecordingStopped() { this.stopCallback?.(new RecorderManagerOnStopResultCallback(this.manager.getFilePath()!)) }6.2 onError 回调:IRecorderManagerFail 与错误码
录音错误事件回调IRecorderManagerFail对象,其字段如下:
| 名称 | 类型 | 必备 | 描述 | | :- | :- | :- | :- | | errCode | number | 是 | 错误码 | | errSubject | string | 是 | 统一错误主题(模块)名称,源码中固定为uni-recorder(见 unierror.uts) | | data | any | 否 | 错误信息中包含的数据 | | cause | Error | 否 | 源错误信息,可以包含多个错误(详见 统一错误规范) | | errMsg | string | 是 | 错误信息 |
errCode 错误码全集
| 合法值 | 兼容性 | 描述 | | :- | :-: | :- | | 1107601 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 没有麦克风权限 | | 1107602 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 不支持该采样率 | | 1107603 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 采样率和编码码率不匹配 | | 1107604 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 启动失败 | | 1107605 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 不支持该音频格式 | | 1107606 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 其他错误 | | 1107607 | Web: x;微信 4.41;Android/iOS/HarmonyOS 4.61 | 被打断 | | 1107608 | Web: x;微信 4.41;Android 4.81;iOS: x;HarmonyOS: x | 正在录音中,请稍后执行此操作 |
以上错误码与英文错误消息的映射定义于 unierror.uts,Android 平台实现会在收到底层录音器错误时,将未知错误码统一映射为1107606(其他错误)后回调给业务层(app-android/index.uts)。
七、平台底层实现与权限机制
7.1 Android 端:运行时权限 + AudioRecorder
Android 实现(app-android/index.uts)在start()时先请求录音运行时权限,授权成功后才真正开始录音;用户拒绝授权则直接回调错误码1107601(没有麦克风权限):
override start(options : RecorderManagerStartOptions) : void { UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, ["android.permission.RECORD_AUDIO"], (allRight : boolean, grantedList : Array<string>) => { this.innerStart(options) }, (allRight : boolean, grantedList : Array<string>) => { this.onRecordingError(1107601, "") }) }因此 Android 端无需开发者额外处理权限请求,拒绝授权时通过onError即可感知。
7.2 HarmonyOS 端:AVRecorder + 条件编译
HarmonyOS 实现(app-harmony/index.uts)基于系统AVRecorder能力,先校验format是否在平台支持格式集合内,不支持则回调错误码1107605;事件注册通过setRecordStateCallback统一管理,并以defineSyncApi(API_GET_RECORDER_MANAGER, ...)注册同步 API。底层音频参数校验(采样率/码率范围、wav 固定参数等)由 AVRecorder.uts 与 utils.uts 实现,对应原文档中 HarmonyOS 专属的码率规则。
7.3 iOS 端:单例共享管理器
iOS 实现(app-ios/index.uts)直接返回UniAudioRecorderManager.shared,具体录音逻辑封装在 UniAudioRecorderManager.swift 中。
7.4 权限配置(重要)
在 HarmonyOS 平台使用录音功能,需要在manifest.json的 HarmonyOS 配置中声明ohos.permission.MICROPHONE权限;Android 端的android.permission.RECORD_AUDIO已由上述requestSystemPermission运行时请求逻辑处理,同时建议在 AndroidManifest 中声明该权限以便发布审核。iOS 端需在Info.plist中配置麦克风用途描述(NSMicrophoneUsageDescription)。各平台权限与原生配置的完整说明可参考 原生配置文件文档 与 应用权限说明。
八、完整可运行示例(uvue)
以下示例为 uni-app x 官方 hello 项目中的录音页完整代码(与最新 HBuilderX Alpha 版同步),覆盖了录音的完整生命周期:格式选择、注册/注销事件、开始/暂停/继续/停止、以及通过uni.createInnerAudioContext()播放录音。示例使用条件编译区分平台特性:HarmonyOS 平台额外提供m4a格式选项,非 HarmonyOS 平台提供pcm选项。
<template> <page-head :title="title"></page-head> <view class="page-body-time"> <text class="time-big">{{formatedRecordTime}}</text> <!-- <text class="time-big">{{recordTimeInterval}}</text> --> </view> <scroll-view style="flex: 1;"> <view> <button class="btnstyle" size="default" @click="registerOnStart">注册onStart</button> <button class="btnstyle" size="default" @click="registeronStop">注册onStop</button> <button class="btnstyle" size="default" id="btn-error" @click="registeronError">注册onError</button> <button class="btnstyle" size="default" @click="registeronPause">注册onPause</button> <button class="btnstyle" size="default" @click="registeronResume">注册onResume</button> <button class="btnstyle" size="default" @click="registeronInterruptionBegin">注册onInterruptionBegin</button> <button class="btnstyle" size="default" @click="registeronInterruptionEnd">注册onInterruptionEnd</button> <view class="uni-list"> <text style="margin-bottom: 10px"> 请选择录音格式:</text> <radio-group class="uni-row" @change="radioChange" style="flex-wrap: wrap"> <view class="uni-list-cell" style="margin-right: 15px" v-for="(item, index) in items" :key="item.value"> <radio :value="item.value" :checked="index === current" /> <text>{{ item.name }}</text> </view> </radio-group> </view> <button class="btnstyle" size="default" :disabled="disableStartBtn" id="btn-startRecord" @click="startRecord()">开始录制</button> <button class="btnstyle" size="default" :disabled="disablePauseBtn" @click="pauseRecord">暂停录制</button> <button class="btnstyle" size="default" :disabled="disableResumeBtn" @click="resumeRecord">继续录制</button> <button class="btnstyle" size="default" id="btn-stopRecord" @click="stopRecord">停止录制</button> <button class="btnstyle" size="default" id="btn-startPlay" @click="playVoice">开始播放</button> <button class="btnstyle" size="default" id="btn-stopPlay" @click="stopVoice">停止播放</button> </view> <bottom-safe-area /> </scroll-view> </template> <script setup lang="uts"> export type ItemType = { value : string, name : string } type DataType = { registerError: boolean, recording: boolean, playing: boolean } const disableStartBtn = ref(false) const disableResumeBtn = ref(false) const disablePauseBtn = ref(false) const title = ref('start/stopRecord、play/stopVoice') const hasRecord = ref(false) //是否有了一个 // 使用reactive避免ref数据在自动化测试中无法访问 const data = reactive({ registerError: false, recording: false, //录音中 playing: false //播放中 } as DataType) const playTimeInterval = ref(0) const recordTimeInterval = ref(0) const tempFilePath = ref('') const recordTime = ref(0) const current = ref(0) const playTime = ref(0) const formatedRecordTime = ref('00:00:00') //录音的总时间 const formatedPlayTime = ref('00:00:00')//播放录音的当前时间, const recorderManager = ref(null as RecorderManager | null) const music = ref(null as InnerAudioContext | null) const items = ref([ { value: 'aac', name: 'aac' }, { value: 'mp3', name: 'mp3' }, { value: 'wav', name: 'wav' }, // #ifdef APP-HARMONY { value: 'm4a', name: 'm4a' }, // #endif // #ifndef APP-HARMONY { value: 'pcm', name: 'pcm' } // #endif ] as ItemType[]) const radioChange = (e: UniRadioGroupChangeEvent) => { for (let i = 0; i < items.value.length; i++) { if (items.value[i].value === e.detail.value) { current.value = i; break; } } } const formatTime = (time: number): string => { if (typeof time !== 'number' || time < 0) { return time.toString() } var hour = parseInt((time / 3600).toString()) time = time % 3600 var minute = parseInt((time / 60).toString()) time = time % 60 var second = time return [hour, minute, second].map((n: number) => { let str = n.toString(); return str.length > 1 ? str : "0" + str; }).join(":"); } const registerOnStart = () => { uni.showToast({ title: 'already registerOnStart' }) recorderManager.value!.onStart(() => { console.log('recorder on start'); recordTime.value = 0 data.recording = true; recordTimeInterval.value = setInterval(() => { recordTime.value += 1; formatedRecordTime.value = formatTime(recordTime.value); }, 1000) }); } const registeronStop = () => { uni.showToast({ title: 'already registeronStop' }) recorderManager.value!.onStop((res) => { console.log('on stop', res); music.value!.src = res.tempFilePath clearInterval(recordTimeInterval.value) hasRecord.value = true; data.recording = false; }); } const registeronError = () => { uni.showToast({ title: 'already registeronError' }) data.registerError = true recorderManager.value!.onError((res) => { console.log('recorder onError', JSON.stringify(res)); }); } const registeronPause = () => { uni.showToast({ title: 'already registeronPause' }) recorderManager.value?.onPause(() => { console.log('recorder onPause'); }) } const registeronResume = () => { uni.showToast({ title: 'already registeronStop' }) recorderManager.value?.onResume(() => { console.log('recorder onResume'); }) } const registeronOnFrameRecorded = () => { uni.showToast({ title: 'already registeronOnFrameRecorded' }) recorderManager.value?.onFrameRecorded((res) => { console.log('recorder onFrameRecorded----', res); }) } const registeronInterruptionBegin = () => { uni.showToast({ title: 'already registeronInterruptionBegin' }) recorderManager.value?.onInterruptionBegin(() => { console.log('recorder onInterruptionBegin'); }) } const registeronInterruptionEnd = () => { uni.showToast({ title: 'already registeronInterruptionEnd' }) recorderManager.value?.onInterruptionEnd(() => { console.log('recorder registeronInterruptionEnd'); }) } const pauseRecord = () => { console.log('recorder pause'); recorderManager.value?.pause() if (data.recording) { disableStartBtn.value = false disablePauseBtn.value = true disableResumeBtn.value = false } clearInterval(recordTimeInterval.value) } const resumeRecord = () => { console.log('recorder resume ', recorderManager.value); recorderManager.value?.resume() recorderManager.value?.onResume(() => { console.log('recorder onResume'); }) if (data.recording) { disableStartBtn.value = false disablePauseBtn.value = false disableResumeBtn.value = true recordTimeInterval.value = setInterval(() => { recordTime.value += 1; formatedRecordTime.value = formatTime(recordTime.value); }, 1000) } } const startRecord = () => { //开始录音 if (data.recording) { uni.showToast({ title: disablePauseBtn.value ? "当前是录音暂停状态" : "当前正在录音" }) return } console.log('startRecord', items.value[current.value].value) // TODO ios 在没有请求过权限之前无法得知是否有相关权限,这种状态下需要直接调用录音,但没有状态或回调判断用户拒绝 recorderManager.value?.start({ format: items.value[current.value].value, sampleRate: 8000, numberOfChannels: 2, encodeBitRate: 48000, frameSize: 2 }); } const stopRecord = () => { //停止录音 recorderManager.value?.stop(); disableStartBtn.value = false disablePauseBtn.value = false disableResumeBtn.value = false } const playVoice = () => { if (data.recording) { uni.showToast({ title: "当前录音还未结束" }) return } console.log('play voice'); if (data.playing) { return } data.playing = true; playTimeInterval.value = setInterval(() => { if (playTime.value < recordTime.value) { playTime.value += 1; } formatedRecordTime.value = formatTime(playTime.value); }, 1000) music.value?.play(); } const stopVoice = () => { if (data.recording) { uni.showToast({ title: "当前录音还未结束" }) return } clearInterval(playTimeInterval.value) data.playing = false; formatedRecordTime.value = formatTime(0); playTime.value = 0; music.value?.stop(); } const end = () => { music.value?.stop(); music.value?.destroy(); // #ifdef APP recorderManager.value?.offError() recorderManager.value?.offFrameRecorded() recorderManager.value?.offInterruptionBegin() recorderManager.value?.offInterruptionEnd() recorderManager.value?.offPause() recorderManager.value?.offResume() recorderManager.value?.offStart() recorderManager.value?.offStop() // #endif recorderManager.value?.stop(); clearInterval(recordTimeInterval.value) clearInterval(playTimeInterval.value); data.recording = false data.playing = false hasRecord.value = false; playTime.value = 0 recordTime.value = 0; formatedRecordTime.value = "00:00:00" formatedRecordTime.value = "00:00:00"; } const clear = () => { end(); } onUnload(() => { end(); }) onLoad(() => { music.value = uni.createInnerAudioContext(); music.value!.onEnded(() => { clearInterval(playTimeInterval.value) var playTimeValue = 0 console.log('play voice finished') data.playing = false; formatedPlayTime.value = formatTime(playTimeValue); playTime.value = playTimeValue; }); recorderManager.value = uni.getRecorderManager(); recorderManager.value!.onStart(() => { console.log('recorder onStart'); disableStartBtn.value = true disablePauseBtn.value = false disableResumeBtn.value = false data.recording = true; recordTime.value = 0 recordTimeInterval.value = setInterval(() => { recordTime.value += 1; formatedRecordTime.value = formatTime(recordTime.value); }, 1000) }); recorderManager.value!.onStop((res) => { console.log('on stop', res.tempFilePath); disablePauseBtn.value = false disableResumeBtn.value = false disableStartBtn.value = false music.value!.src = res.tempFilePath clearInterval(recordTimeInterval.value) hasRecord.value = true; data.recording = false; }); recorderManager.value!.onError((res) => { console.log('recorder onError', JSON.stringify(res)); disablePauseBtn.value = false disableResumeBtn.value = false disableStartBtn.value = false data.registerError = true uni.showToast({ title: JSON.stringify(res) }) }); }) defineExpose({ data }) </script> <style> .page-body-time { display: flex; flex-direction: column; align-items: center; } .time-big { font-size: 30px; margin: 10px; } .btnstyle { margin-left: 30px; margin-right: 30px; margin-top: 10px; } .uni-list { border-bottom: 0px; background-color: transparent; margin-left: 30px; margin-right: 30px; margin-top: 10px; margin-bottom: 10px; } </style>该示例的源码版本位于本仓库 src/pages/API/get-recorder-manager/get-recorder-manager.uvue,可在 hello uni-app x 工程的 API 页面中直接体验。注意示例中
frameSize: 2仅在微信小程序端配合 mp3 生效,App 端会忽略该字段。
示例中的关键编码要点
- 事件注册时机:
onLoad中一次性注册onStart/onStop/onError,页面卸载(onUnload)时通过end()统一offXxx注销监听并停止录音,避免单例管理器上的监听泄漏; - 录制中状态机:通过
disableStartBtn/disablePauseBtn/disableResumeBtn三个按钮禁用状态维护"录制中 / 暂停中 / 空闲"三态流转,防止在录音中重复start()(会触发错误码 1107608); - 录音计时与回放计时:
setInterval每秒累计recordTime并格式化为HH:MM:SS,停止录音后通过music.value!.src = res.tempFilePath将临时文件交给uni.createInnerAudioContext()播放; - iOS 权限提示:源码注释明确指出,iOS 在未请求过权限之前无法得知是否已有麦克风权限,需要直接调用录音,且没有回调可判断用户拒绝,因此 iOS 端若需弹权限引导,建议结合其他权限 API 自行处理。
九、常见问题排查指引
| 现象 | 可能原因 | 处理建议 | | :- | :- | :- | | onError 返回 1107601 | 用户未授予麦克风权限(Android 拒绝授权 / iOS 未请求 / HarmonyOS 未声明 ohos.permission.MICROPHONE) | Android 端重试 start 会重新拉起权限弹窗;HarmonyOS 检查 manifest 权限声明;iOS 引导用户到设置开启 | | onError 返回 1107602 | 传入了非 8000/16000/44100 的采样率 | 检查 sampleRate 取值 | | onError 返回 1107603 | 采样率与编码码率超出匹配表范围 | 按第五节表格调整 encodeBitRate | | onError 返回 1107604 | 录音启动失败(麦克风被占用等) | 检查是否有其他应用占用麦克风,稍后重试 | | onError 返回 1107605 | format 传入了平台不支持的格式(如 iOS 传 mp3/m4a、App 端传 m4a 但非 HarmonyOS) | 结合第四节 format 兼容性表选择格式 | | onError 返回 1107607 | 录音被系统打断(如来电、其他应用占用录音) | 监听 onInterruptionBegin / onInterruptionEnd 做业务提示与恢复 | | onError 返回 1107608 | 录音尚未停止时再次调用了 start | 用状态标记阻止重复 start | | 录音文件无法播放 | format 与播放器支持不匹配,或未等 onStop 回调就读取文件 | 使用 onStop 回调中的 tempFilePath,并用兼容的播放器(如 createInnerAudioContext)播放 |
十、更多资料
- 录音管理相关已知问题与反馈入口参见 相关 Bug;
- uni-app(Vue 语法)版本的录音文档可参考 uni-app 录音管理文档;
- 各小程序平台(微信、支付宝、百度、抖音、飞书、钉钉、QQ、快手、京东、华为快应用、360)的 RecorderManager 实现细节以各平台官方小程序文档为准;
- 模块实现源码:src/uni_modules/uni-recorder、示例页面 get-recorder-manager.uvue、协议声明 protocol.uts、错误定义 unierror.uts。
- 示例工程
- 前端
- 移动开发
- 跨平台
【免费下载链接】uni-app
A cross-platform framework using Vue.js
相关推荐
uni-app 录音管理深入解析:uni.getRecorderManager 与 uni-recorder UTS 插件源码剖析
uni app 录音管理深入解析:uni.getRecorderManager 与 uni recorder UTS 插件源码剖析 uni recorder 是
示例工程前端移动开发跨平台uni-app x 图片预览插件 uni-previewImage 完全指南:API 详解、源码剖析与 UTS 插件实践
uni app x 图片预览插件 uni previewImage 完全指南:API 详解、源码剖析与 UTS 插件实践 uni previewImage 是
示例工程前端移动开发跨平台uni-app x 视频保存到相册指南:uni.saveVideoToPhotosAlbum 全平台实现与源码剖析
uni app x 视频保存到相册指南:uni.saveVideoToPhotosAlbum 全平台实现与源码剖析 uni.saveVideoToPhotosA
示例工程前端移动开发跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考