如何结合AudioPlaybackCapture API与MediaRecorder捕获系统音频
如何结合AudioPlaybackCapture API与MediaRecorder捕获系统音频
首先得明确:你之前尝试的方法失败是因为AudioRecord.getAudioSource()返回的是REMOTE_SUBMIX(也就是r_submix)的ID,这个音频源需要系统签名权限,普通应用根本拿不到,所以这条路走不通。
而MediaRecorder作为高层API,本身不支持直接接入AudioPlaybackCapture的音频流——它的音频源参数只能用系统预定义的几个常量(比如MIC、DEFAULT等),没法直接用动态创建的AudioPlaybackCapture配置。不过别担心,我们可以用「分开捕获+后期合并」的方案,既保留你现有MediaRecorder的录屏逻辑,又能通过AudioPlaybackCapture拿到系统音频。
核心思路
- 继续用MediaRecorder捕获屏幕视频(可以选择不录麦克风音频,或者同时录,看你的需求)
- 用AudioRecord+AudioPlaybackCapture单独捕获系统音频,保存为PCM文件或者直接编码为AAC
- 录制完成后,把视频文件和音频文件合并成一个完整的音视频文件
这个方案不需要你替换现有大量的MediaRecorder代码,只是额外加一套音频捕获和合并的逻辑。
具体实现步骤
1. 权限准备
确保你的Manifest里声明了必要权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 如果存外部存储的话 -->
另外,你需要通过MediaProjectionManager获取屏幕录制权限(这是录屏的必要步骤,你应该已经在做了)。
2. 用MediaRecorder捕获屏幕视频
这部分你应该已经有成熟的代码了,核心是不要设置音频源(或者如果需要麦克风音频可以设置MediaRecorder.AudioSource.MIC),专注录视频:
MediaRecorder mMediaRecorder = new MediaRecorder(); // 配置视频相关参数 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setVideoSize(screenWidth, screenHeight); mMediaRecorder.setVideoEncodingBitRate(6000000); // 按需调整 mMediaRecorder.setOutputFile(videoOutputPath); // 视频输出路径 // 准备并启动录制 mMediaRecorder.prepare(); Surface captureSurface = mMediaRecorder.getSurface(); // 通过MediaProjection创建虚拟显示器,把屏幕内容输出到MediaRecorder的Surface MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay( "ScreenCapture", screenWidth, screenHeight, getResources().getDisplayMetrics().densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, captureSurface, null, null); mMediaRecorder.start();
3. 用AudioPlaybackCapture捕获系统音频
同时启动一个AudioRecord来捕获系统音频,注意要和MediaProjection关联:
// 配置AudioPlaybackCapture,指定要捕获的音频类型(媒体、游戏等) AudioPlaybackCaptureConfiguration audioCaptureConfig = new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) .addMatchingUsage(AudioAttributes.USAGE_GAME) .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) .build(); // 定义音频格式 AudioFormat audioFormat = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(44100) // 常用采样率,按需调整 .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) .build(); // 计算最小缓冲区大小 int bufferSize = AudioRecord.getMinBufferSize( audioFormat.getSampleRate(), audioFormat.getChannelMask(), audioFormat.getEncoding()) * 2; // 创建AudioRecord实例 AudioRecord audioRecord = new AudioRecord.Builder() .setAudioPlaybackCaptureConfig(audioCaptureConfig) .setAudioFormat(audioFormat) .setBufferSizeInBytes(bufferSize) .build(); // 启动音频录制线程 isRecording = true; new Thread(() -> { byte[] audioBuffer = new byte[bufferSize]; FileOutputStream fos = null; try { fos = new FileOutputStream(audioOutputPath); // 保存PCM音频的路径 audioRecord.startRecording(); while (isRecording) { int readBytes = audioRecord.read(audioBuffer, 0, bufferSize); if (readBytes > 0) { fos.write(audioBuffer, 0, readBytes); } } } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } audioRecord.stop(); audioRecord.release(); } }).start();
4. 合并音视频文件
录制完成后,你需要把视频文件和PCM音频文件合并成一个完整的MP4。这里可以用Android自带的MediaMuxer,不过需要先把PCM编码为AAC(因为MediaMuxer不支持原始PCM轨道)。
编码PCM到AAC的简单示例
private void encodePcmToAac(String pcmPath, String aacPath) throws IOException { MediaCodec audioCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); MediaFormat audioFormat = MediaFormat.createAudioFormat( "audio/mp4a-latm", 44100, 2); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000); audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); audioCodec.start(); FileInputStream fis = new FileInputStream(pcmPath); FileOutputStream fos = new FileOutputStream(aacPath); ByteBuffer[] inputBuffers = audioCodec.getInputBuffers(); ByteBuffer[] outputBuffers = audioCodec.getOutputBuffers(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); boolean isPcmEnd = false; long presentationTimeUs = 0; final long frameDurationUs = (1000000 * 1024) / 44100; // 按1024帧计算 while (!isPcmEnd) { int inputBufferIndex = audioCodec.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); int readBytes = fis.read(inputBuffer.array(), inputBuffer.arrayOffset(), inputBuffer.capacity()); if (readBytes < 0) { audioCodec.queueInputBuffer(inputBufferIndex, 0, 0, presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); isPcmEnd = true; } else { audioCodec.queueInputBuffer(inputBufferIndex, 0, readBytes, presentationTimeUs, 0); presentationTimeUs += frameDurationUs; } } int outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, -1); while (outputBufferIndex >= 0) { ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; byte[] outData = new byte[bufferInfo.size]; outputBuffer.get(outData); fos.write(outData); audioCodec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, -1); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } fis.close(); fos.close(); audioCodec.stop(); audioCodec.release(); }
用MediaMuxer合并视频和AAC
private void mergeVideoAndAudio(String videoPath, String aacPath, String outputPath) throws IOException { MediaExtractor videoExtractor = new MediaExtractor(); videoExtractor.setDataSource(videoPath); int videoTrackIndex = -1; for (int i = 0; i < videoExtractor.getTrackCount(); i++) { MediaFormat format = videoExtractor.getTrackFormat(i); if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) { videoTrackIndex = i; break; } } videoExtractor.selectTrack(videoTrackIndex); MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex); MediaExtractor audioExtractor = new MediaExtractor(); audioExtractor.setDataSource(aacPath); int audioTrackIndex = -1; for (int i = 0; i < audioExtractor.getTrackCount(); i++) { MediaFormat format = audioExtractor.getTrackFormat(i); if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) { audioTrackIndex = i; break; } } audioExtractor.selectTrack(audioTrackIndex); MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrackIndex); MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); int videoMuxTrack = muxer.addTrack(videoFormat); int audioMuxTrack = muxer.addTrack(audioFormat); muxer.start(); ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); // 写入视频数据 while (true) { int readSize = videoExtractor.readSampleData(buffer, 0); if (readSize < 0) { break; } bufferInfo.offset = 0; bufferInfo.size = readSize; bufferInfo.presentationTimeUs = videoExtractor.getSampleTime(); bufferInfo.flags = videoExtractor.getSampleFlags(); muxer.writeSampleData(videoMuxTrack, buffer, bufferInfo); videoExtractor.advance(); } // 写入音频数据 while (true) { int readSize = audioExtractor.readSampleData(buffer, 0); if (readSize < 0) { break; } bufferInfo.offset = 0; bufferInfo.size = readSize; bufferInfo.presentationTimeUs = audioExtractor.getSampleTime(); bufferInfo.flags = audioExtractor.getSampleFlags(); muxer.writeSampleData(audioMuxTrack, buffer, bufferInfo); audioExtractor.advance(); } muxer.stop(); muxer.release(); videoExtractor.release(); audioExtractor.release(); }
关键注意事项
- 系统版本要求:AudioPlaybackCapture API仅在Android 10(API 29)及以上可用
- 前台限制:应用必须处于前台才能进行音频捕获,后台会被系统限制
- 音视频同步:合并时要确保音频和视频的时间戳对齐,避免出现音画不同步的问题
- 权限检查:除了Manifest声明,还要动态申请
RECORD_AUDIO权限,以及通过MediaProjection获取屏幕录制权限
内容的提问来源于stack exchange,提问作者Vijai




