HarmonyOS 7 音频与媒体控制实战 02:实现系统音频内录与状态控制

上一篇做了应用内录音,录的是麦克风输入。产品又提了个需求:能不能录系统内部播放的声音?比如用户在听网课、看视频,想把系统播放的音频录下来存成文件。这就是系统音频内录,和麦克风录音完全是两回事。
最开始我以为改一下 AudioCapturer 的 source 类型就行,结果真做的时候发现坑很多——授权流程不一样、前后台切换会中断、录出来的声音可能是空的。这篇就讲讲系统音频内录怎么实现,以及哪些地方容易踩坑。
一、真实开发中遇到的问题
最开始照着麦克风录音的代码改,把 source 从 MIC 改成系统音频类型,结果跑起来直接报错。查了文档才知道,系统音频录制需要特殊的权限和授权流程,不是改个参数就行。
第一个问题:授权流程不一样。麦克风录音只要申请 MICROPHONE 权限就行,系统音频内录需要用户在系统设置里手动开启"应用可以录制系统音频"的开关。这个开关在哪?怎么引导用户去开?
第二个问题:前后台切换。用户切到别的应用去播放音乐,我们的录音服务在后台,还能继续录吗?切回来的时候录音状态怎么同步?
第三个问题:录出来的声音是空的。授权开了,录音也启动了,但文件里全是静音。这是怎么回事?
第四个问题:录制中断。录到一半用户接了个电话,或者别的应用抢了音频焦点,录音就停了。怎么监听中断事件,怎么恢复?
二、这个能力怎么接入
系统音频内录的接入流程比麦克风录音复杂:
1. 检查授权状态:调用接口查询当前应用是否有系统音频录制权限。如果没有,引导用户去系统设置里开。
2. 配置 AudioCapturer:source 类型选系统音频(STREAM_TYPE_MUSIC 或者对应的系统音频源),采样率和声道数要和系统播放的音频匹配。
3. 前后台生命周期管理:应用切到后台的时候,录音要继续(需要声明后台任务),切回来的时候同步状态。
4. 监听音频中断事件:来电、其他应用抢占音频焦点的时候,系统会发中断通知,我们要响应这个事件,暂停或停止录音。
这里最关键的是授权流程——用户必须手动去设置里开开关,我们不能直接弹窗授权。

三、关键代码怎么写
下面是封装的系统音频录制器,文件位置在 entry/src/main/ets/utils/SystemAudioRecorder.ets:
import { audio } from '@kit.AudioKit';
import { fileIo as fs } from '@kit.CoreFileKit';
export class SystemAudioRecorder {
private capturer: audio.AudioCapturer | null = null;
private fileFd: fs.File | null = null;
private isRecording: boolean = false;
// 检查系统音频录制权限
async checkPermission(): Promise<boolean> {
const audioManager = audio.getAudioManager();
const volumeManager = audioManager.getVolumeManager();
// 查询系统音频录制权限状态
try {
const granted = await volumeManager.isUsingAudioCaptureAllowed();
return granted;
} catch (e) {
console.error('Check permission failed: ' + e.message);
return false;
}
}
// 引导用户去设置页开启权限
async openPermissionSettings(): Promise<void> {
// 跳转到系统音频录制权限设置页
const context = getContext(this) as common.UIAbilityContext;
want = {
action: 'action.settings.app.info',
parameters: {
settingsParam: 'audio_capture'
}
};
await context.startAbility(want);
}
// 初始化录制器
async initCapturer(): Promise<void> {
const capturerInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_REMOTE_CAST,
capturerFlags: 0
};
const streamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
this.capturer = await audio.createAudioCapturer({
streamInfo: streamInfo,
capturerInfo: capturerInfo
});
// 注册音频中断监听
this.capturer.on('interrupt', (event) => {
if (event.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_PAUSE) {
// 被强制中断,暂停录音
console.info('Recording interrupted by force');
this.pauseRecording();
} else if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_STOP) {
// 被停止,结束录音
this.stopRecording();
}
}
});
}
// 开始录制
async startRecording(filePath: string): Promise<void> {
// 先检查权限
const hasPermission = await this.checkPermission();
if (!hasPermission) {
throw new Error('需要先在系统设置中开启系统音频录制权限');
}
if (!this.capturer) {
await this.initCapturer();
}
this.fileFd = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
this.isRecording = true;
await this.capturer!.start();
// 子线程读取数据
this.startReadThread();
}
// 读取线程
private startReadThread(): void {
const bufferSize = this.capturer!.getBufferSize();
const buffer = new ArrayBuffer(bufferSize);
while (this.isRecording) {
const bytesRead = this.capturer!.read(buffer, bufferSize);
if (bytesRead > 0 && this.fileFd) {
fs.writeSync(this.fileFd.fd, buffer);
}
}
}
async pauseRecording(): Promise<void> {
if (this.capturer && this.isRecording) {
await this.capturer.pause();
}
}
async resumeRecording(): Promise<void> {
if (this.capturer && this.isRecording) {
await this.capturer.resume();
}
}
async stopRecording(): Promise<void> {
this.isRecording = false;
if (this.capturer) {
await this.capturer.stop();
}
if (this.fileFd) {
fs.closeSync(this.fileFd);
this.fileFd = null;
}
}
}
这段代码的核心:checkPermission 先查权限,没有权限引导用户去设置页。source 用 SOURCE_TYPE_REMOTE_CAST 录制系统内部音频。注册了 interrupt 事件监听,被系统强制中断的时候自动暂停或停止。
页面里怎么引导用户授权?文件位置 pages/SystemRecordPage.ets:
@Entry
@Component
struct SystemRecordPage {
@State hasPermission: boolean = false;
@State isRecording: boolean = false;
private recorder: SystemAudioRecorder = new SystemAudioRecorder();
async aboutToAppear() {
this.hasPermission = await this.recorder.checkPermission();
}
async onStartRecord() {
// 没有权限先引导去设置
if (!this.hasPermission) {
this.recorder.openPermissionSettings();
prompt.showToast({ message: '请在设置中开启系统音频录制权限' });
return;
}
try {
const filePath = getContext(this).filesDir + '/system_' + Date.now() + '.pcm';
await this.recorder.startRecording(filePath);
this.isRecording = true;
} catch (e) {
prompt.showToast({ message: '录制失败:' + e.message });
}
}
build() {
Column() {
if (!this.hasPermission) {
Text('需要开启系统音频录制权限')
Button('去设置开启')
.onClick(() => this.recorder.openPermissionSettings())
} else {
Button(this.isRecording ? '停止录制' : '开始录制')
.onClick(() => {
if (this.isRecording) {
this.recorder.stopRecording();
this.isRecording = false;
} else {
this.onStartRecord();
}
})
}
}
.padding(24)
}
}

四、运行过程中怎么处理异常
系统音频内录的异常场景:
1. 权限没开:checkPermission 返回 false,直接引导用户去设置页。不要尝试用麦克风权限替代。
2. 录制中断:来电、闹钟响了、其他应用开始播放音频,系统会发 interrupt 事件。我们监听这个事件,强制中断的时候暂停录音,友好中断的时候给个提示让用户决定。
3. 录出来是空的:系统正在播放的音频流类型和我们录制的不匹配。比如我们录的是音乐流,但系统播放的是通知音,就录不到。要选对 source 类型。
4. 前后台切换:应用切到后台了,录制服务要继续运行,需要声明后台任务。不然系统直接把应用杀了,录制就停了。
5. 用户取消授权:录到一半用户去设置里把权限关了,下一次 startRecording 就会失败。要 catch 住异常,提示用户权限已被关闭。
五、实际开发中容易忽略的问题
1. 权限不是申请来的:系统音频录制权限不是运行时弹窗授权,必须用户手动去设置里开。我们能做的就是引导用户去设置页,以及在设置页开启后回来重新检查状态。
2. 采样率要匹配:系统播放的音频采样率可能是 48kHz,如果我们配成 44.1kHz,录出来的声音会变速变调。最好用 48kHz 立体声,和系统播放端匹配。
3. 后台录制要声明:纯后台录制需要 ohos.permission.KEEP_BACKGROUND_RUNNING 权限,以及在 module.json5 里声明后台任务类型。不然切到后台就停了。
4. 中断事件要监听:不监听 interrupt 事件,录到一半被打断了都不知道,用户还以为在录,结果文件是空的。
5. 隐私提示:录系统音频是敏感操作,开始录制前要明确提示用户"即将录制系统播放的所有音频",让用户知情。
系统音频内录和麦克风录音思路完全不同。麦克风录音只要权限和参数对了就能跑,系统音频内录要处理授权流程、前后台切换、中断事件,工程复杂度高不少。下一篇讲音频实时效果处理,把录到的声音加特效。
更多推荐



所有评论(0)