⾳频录制与播放:后台录⾳、语⾳上传、资源释放避坑
·
一、前言
语音相关能力在鸿蒙项目里十分常见,语音笔记、客服语音消息、语音转文字、音频回放上传等业务都离不开音频模块。很多开发在调试时,前台录音播放一切正常,一旦退到后台、频繁启停音频,就会遇到各种问题:后台录音中断、再次打开提示音频占用、上传后资源没释放导致内存持续上涨,甚至直接触发应用闪退。
这类问题大多不是 API 调用错误,而是生命周期和资源管理没有处理到位。本文基于最新鸿蒙 API,讲解音频录制、播放、后台保活、文件上传完整流程,所有代码可直接复制运行。
二、权限前置配置
音频能力需要在module.json5配置静态权限,同时代码中动态申请敏感权限。
// module.json5
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:mic_permission_reason",
"usedScene": {
"abilities": ["./EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:bg_permission_reason",
"usedScene": {
"abilities": ["./EntryAbility"],
"when": "inuse"
}
}
]
说明:MICROPHONE麦克风权限属于敏感权限,必须动态申请;后台持续录音,需要申请后台运行权限,上架时权限说明要写清楚业务用途。
三、音频录制实现(支持后台录音)
使用 AudioCapturer 完成音频采集,录制的音频保存到应用沙箱目录。后台录音核心要点:启动录音前申请后台任务,防止应用切后台被系统挂起。
import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
import { backgroundTaskManager } from '@kit.BackgroundTaskKit';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct AudioRecordDemo {
private audioCapturer: audio.AudioCapturer | null = null;
private fileFd: number = -1;
private bgTaskId: number = -1;
@State isRecording: boolean = false;
private filePath: string = '';
// 初始化音频采集器
async initCapturer() {
const capturerInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE
}
};
this.audioCapturer = await audio.createAudioCapturer(capturerInfo);
}
// 开始录音,申请后台任务
async startRecord() {
if (this.isRecording) return;
await this.initCapturer();
const context = getContext(this);
this.filePath = `${context.filesDir}/record_audio.pcm`;
this.fileFd = await fileIo.open(this.filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
// 申请后台长时任务,保障后台录音
this.bgTaskId = await backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK);
await this.audioCapturer?.start();
this.isRecording = true;
this.readAudioData();
}
// 循环读取音频数据流写入文件
async readAudioData() {
if (!this.isRecording || !this.audioCapturer) return;
const buffer = await this.audioCapturer.read();
if (buffer.length > 0 && this.fileFd > 0) {
await fileIo.write(this.fileFd, buffer);
}
setTimeout(() => this.readAudioData(), 10);
}
// 停止录音,释放资源
async stopRecord() {
if (!this.isRecording || !this.audioCapturer) return;
await this.audioCapturer.stop();
this.isRecording = false;
await fileIo.close(this.fileFd);
// 结束后台任务
if (this.bgTaskId !== -1) {
const ctx = getContext(this);
backgroundTaskManager.stopBackgroundRunning(ctx, this.bgTaskId);
this.bgTaskId = -1;
}
// 释放音频实例,避免占用
this.audioCapturer.release();
this.audioCapturer = null;
promptAction.showToast({message: "录音已保存"});
}
build() {
Column() {
Row() {
Button(this.isRecording ? "停止录音" : "开始录音")
.onClick(() => {
if(this.isRecording) {
this.stopRecord()
} else {
this.startRecord()
}
})
}
.margin(20)
}
.width('100%')
}
}
四、音频播放实现
AudioRenderer 用来播放沙箱内录制好的 pcm 音频文件,用完必须 release 释放实例。
import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
@Component
struct AudioPlayDemo {
private audioRenderer: audio.AudioRenderer | null = null;
private fileFd: number = -1;
async initRenderer() {
const renderInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE
}
}
this.audioRenderer = await audio.createAudioRenderer(renderInfo);
}
async startPlay(filePath: string) {
await this.initRenderer();
this.fileFd = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
await this.audioRenderer?.start();
this.playLoop();
}
async playLoop() {
if (!this.audioRenderer) return;
const buf = new ArrayBuffer(4096);
const readLen = await fileIo.read(this.fileFd, buf);
if(readLen > 0) {
await this.audioRenderer.write(buf);
this.playLoop();
} else {
await this.stopPlay();
}
}
async stopPlay() {
if(!this.audioRenderer) return;
await this.audioRenderer.stop();
await fileIo.close(this.fileFd);
this.audioRenderer.release();
this.audioRenderer = null;
}
build() {
Button("播放录音")
.onClick(async () => {
const ctx = getContext(this);
await this.startPlay(`${ctx.filesDir}/record_audio.pcm`)
})
}
}
五、音频文件上传
录音完成后,读取沙箱音频文件,通过网络模块上传到后端。
import { http } from '@kit.NetworkKit';
import { fileIo } from '@kit.CoreFileKit';
async function uploadAudio(filePath: string) {
const fileFd = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const stat = await fileIo.stat(filePath);
const buf = new ArrayBuffer(stat.size as number);
await fileIo.read(fileFd, buf);
const httpReq: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
extraData: buf,
header: {
"Content-Type": "application/octet-stream"
}
}
const res = await http.request("https://xxx/upload", httpReq);
console.info("上传结果", res.result);
await fileIo.close(fileFd);
}
六、资源释放避坑重点
音频硬件属于系统独占资源,这也是最容易踩坑的地方。
- 实例释放:AudioCapturer、AudioRenderer 使用结束,一定要调用 release,不能单纯 stop。只 stop 不 release,音频硬件会被持续占用,下次启动录音直接报错。
- 文件句柄:打开的 fd 必须 close。反复录制不关闭句柄,会耗尽应用文件描述符,触发 IO 异常。
- 后台任务配对:startBackgroundRunning 之后,必须成对调用 stopBackgroundRunning。如果应用异常退出,系统会自动回收,但正常业务流程一定要手动结束后台任务,否则会影响应用功耗评分。
- 页面销毁:页面销毁时,如果录音 / 播放还在进行,要主动停止并释放所有实例。页面生命周期 onDisappear 里增加资源回收逻辑。
- 并发限制:不要同时创建多个 AudioCapturer 实例,同一时刻只允许一个录音实例。
七、小结
音频模块开发,难点不在于采集和播放本身,而是资源管理和后台运行的配合。录音启动时申请后台任务保证后台持续采集,结束时依次停止采集、关闭文件句柄、释放音频实例、关闭后台任务。
录音文件上传完成之后,可根据业务需求选择保留或删除本地 pcm 文件,减少存储空间占用。把资源释放逻辑封装到统一工具类,在页面销毁、录音中断等场景统一调用,就能规避绝大多数音频占用、内存泄漏类问题。
更多推荐


所有评论(0)