You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何结合AudioPlaybackCapture API与MediaRecorder捕获系统音频

如何结合AudioPlaybackCapture API与MediaRecorder捕获系统音频

首先得明确:你之前尝试的方法失败是因为AudioRecord.getAudioSource()返回的是REMOTE_SUBMIX(也就是r_submix)的ID,这个音频源需要系统签名权限,普通应用根本拿不到,所以这条路走不通。

而MediaRecorder作为高层API,本身不支持直接接入AudioPlaybackCapture的音频流——它的音频源参数只能用系统预定义的几个常量(比如MIC、DEFAULT等),没法直接用动态创建的AudioPlaybackCapture配置。不过别担心,我们可以用「分开捕获+后期合并」的方案,既保留你现有MediaRecorder的录屏逻辑,又能通过AudioPlaybackCapture拿到系统音频。

核心思路

  1. 继续用MediaRecorder捕获屏幕视频(可以选择不录麦克风音频,或者同时录,看你的需求)
  2. 用AudioRecord+AudioPlaybackCapture单独捕获系统音频,保存为PCM文件或者直接编码为AAC
  3. 录制完成后,把视频文件和音频文件合并成一个完整的音视频文件

这个方案不需要你替换现有大量的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

火山引擎 最新活动