You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求Android MediaMuxer录制SurfaceView+麦克风音频生成MP4的完整示例代码

Got it, I totally get why you're frustrated—combining SurfaceView capture with microphone audio using MediaMuxer is trickier than it seems, especially since most examples (like Grafika) only handle video alone. Below is a complete, working implementation that captures your SurfaceView's content via MediaProjection, records microphone audio with AudioRecord, and muxes both into a valid MP4 file.

Core Approach

Here's the high-level flow to make this work:

  • Use MediaProjection to create a VirtualDisplay that mirrors your SurfaceView (or adjust to target just the SurfaceView's area if needed).
  • Capture video frames from the VirtualDisplay using a Surface that feeds into MediaCodec for H.264 encoding.
  • Use AudioRecord to capture raw microphone audio, then feed it into another MediaCodec for AAC encoding.
  • Sync the encoded video and audio frames using a shared start timestamp, then write both to MediaMuxer to produce the final MP4.

Full Working Example

First, add these permissions to your AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />

Activity Layout (activity_main.xml)

A simple layout with a SurfaceView and control buttons:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <Button
        android:id="@+id/btnStartRecord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Recording" />

    <Button
        android:id="@+id/btnStopRecord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Recording"
        android:enabled="false" />
</LinearLayout>

Main Activity Code (MainActivity.java)

This handles permission requests, MediaProjection setup, video/audio capture, and muxing:

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.os.Environment;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "SurfaceAudioRecorder";
    private static final int REQUEST_CODE_PERMISSIONS = 1001;
    private static final int REQUEST_CODE_MEDIA_PROJECTION = 1002;

    private MediaProjectionManager mMediaProjectionManager;
    private MediaProjection mMediaProjection;
    private android.media.projection.VirtualDisplay mVirtualDisplay;
    private MediaCodec mVideoCodec;
    private MediaCodec mAudioCodec;
    private MediaMuxer mMediaMuxer;
    private AudioRecord mAudioRecord;

    private SurfaceView mSurfaceView;
    private Button mBtnStart, mBtnStop;

    private int mScreenWidth, mScreenHeight;
    private int mVideoTrackIndex = -1, mAudioTrackIndex = -1;
    private boolean mMuxerStarted = false;
    private volatile boolean mIsRecording = false;
    private long mGlobalStartTimeNs;

    private static final String MIME_TYPE_VIDEO = "video/avc"; // H.264
    private static final String MIME_TYPE_AUDIO = "audio/mp4a-latm"; // AAC
    private static final int VIDEO_BIT_RATE = 6000000; // 6Mbps
    private static final int FRAME_RATE = 30;
    private static final int I_FRAME_INTERVAL = 2; // 2 seconds between keyframes

    // Audio params
    private static final int AUDIO_SAMPLE_RATE = 44100;
    private static final int AUDIO_CHANNEL_COUNT = 1; // Mono
    private static final int AUDIO_BIT_RATE = 128000; // 128kbps

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSurfaceView = findViewById(R.id.surfaceView);
        mBtnStart = findViewById(R.id.btnStartRecord);
        mBtnStop = findViewById(R.id.btnStopRecord);

        mBtnStart.setOnClickListener(this);
        mBtnStop.setOnClickListener(this);

        mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

        // Get screen dimensions
        DisplayMetrics metrics = getResources().getDisplayMetrics();
        mScreenWidth = metrics.widthPixels;
        mScreenHeight = metrics.heightPixels;
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btnStartRecord) {
            checkPermissionsAndStartRecording();
        } else if (v.getId() == R.id.btnStopRecord) {
            stopRecording();
        }
    }

    private void checkPermissionsAndStartRecording() {
        String[] requiredPermissions = {
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        };

        boolean allGranted = true;
        for (String permission : requiredPermissions) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }

        if (allGranted) {
            startMediaProjection();
        } else {
            ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_CODE_PERMISSIONS);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            boolean allGranted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    allGranted = false;
                    break;
                }
            }
            if (allGranted) {
                startMediaProjection();
            } else {
                Log.e(TAG, "Permissions not granted");
            }
        }
    }

    private void startMediaProjection() {
        Intent intent = mMediaProjectionManager.createScreenCaptureIntent();
        startActivityForResult(intent, REQUEST_CODE_MEDIA_PROJECTION);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE_MEDIA_PROJECTION && resultCode == RESULT_OK) {
            mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
            initRecording();
            startRecording();
        }
    }

    private void initRecording() {
        try {
            // Create output file in Movies directory
            File outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
                    "SurfaceAudioRecord_" + System.currentTimeMillis() + ".mp4");
            mMediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

            // Initialize video and audio codecs
            initVideoCodec();
            initAudioCodec();

        } catch (IOException e) {
            Log.e(TAG, "Failed to initialize recording", e);
        }
    }

    private void initVideoCodec() throws IOException {
        MediaFormat videoFormat = MediaFormat.createVideoFormat(MIME_TYPE_VIDEO, mScreenWidth, mScreenHeight);
        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BIT_RATE);
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);

        mVideoCodec = MediaCodec.createEncoderByType(MIME_TYPE_VIDEO);
        mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        Surface inputSurface = mVideoCodec.createInputSurface();

        // Create VirtualDisplay to capture screen (adjust bounds to target only SurfaceView if needed)
        mVirtualDisplay = mMediaProjection.createVirtualDisplay(
                "SurfaceCapture",
                mScreenWidth, mScreenHeight,
                getResources().getDisplayMetrics().densityDpi,
                0 /* flags */,
                inputSurface,
                null /* callback */,
                null /* handler */);
    }

    private void initAudioCodec() throws IOException {
        MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE_AUDIO, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL_COUNT);
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BIT_RATE);
        audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);

        mAudioCodec = MediaCodec.createEncoderByType(MIME_TYPE_AUDIO);
        mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        // Initialize AudioRecord for microphone capture
        int audioBufferSize = AudioRecord.getMinBufferSize(
                AUDIO_SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT);
        mAudioRecord = new AudioRecord(
                android.media.MediaRecorder.AudioSource.MIC,
                AUDIO_SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT,
                audioBufferSize * 2);
    }

    private void startRecording() {
        mIsRecording = true;
        mBtnStart.setEnabled(false);
        mBtnStop.setEnabled(true);
        mGlobalStartTimeNs = System.nanoTime(); // Shared start time for sync

        // Start video and audio encoding in separate threads
        new Thread(new VideoEncoderRunnable()).start();
        new Thread(new AudioEncoderRunnable()).start();
    }

    private void stopRecording() {
        mIsRecording = false;
        mBtnStart.setEnabled(true);
        mBtnStop.setEnabled(false);

        // Release resources in safe order to avoid crashes
        if (mAudioRecord != null) {
            mAudioRecord.stop();
            mAudioRecord.release();
            mAudioRecord = null;
        }

        if (mAudioCodec != null) {
            mAudioCodec.stop();
            mAudioCodec.release();
            mAudioCodec = null;
        }

        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
            mVirtualDisplay = null;
        }

        if (mVideoCodec != null) {
            mVideoCodec.stop();
            mVideoCodec.release();
            mVideoCodec = null;
        }

        if (mMediaProjection != null) {
            mMediaProjection.stop();
            mMediaProjection = null;
        }

        if (mMediaMuxer != null) {
            try {
                mMediaMuxer.stop();
                mMediaMuxer.release();
            } catch (IllegalStateException e) {
                // Ignore if muxer wasn't started
            }
            mMediaMuxer = null;
        }

        // Reset state
        mMuxerStarted = false;
        mVideoTrackIndex = -1;
        mAudioTrackIndex = -1;
    }

    private class VideoEncoderRunnable implements Runnable {
        @Override
        public void run() {
            mVideoCodec.start();
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

            while (mIsRecording) {
                int outputBufferId = mVideoCodec.dequeueOutputBuffer(bufferInfo, 10000);
                if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    // Add video track to muxer once format is available
                    if (mVideoTrackIndex == -1) {
                        MediaFormat format = mVideoCodec.getOutputFormat();
                        mVideoTrackIndex = mMediaMuxer.addTrack(format);
                        startMuxerIfReady();
                    }
                } else if (outputBufferId >= 0) {
                    ByteBuffer outputBuffer = mVideoCodec.getOutputBuffer(outputBufferId);
                    if (bufferInfo.size > 0 && mMuxerStarted) {
                        // Sync timestamp to global start time
                        bufferInfo.presentationTimeUs = (System.nanoTime() - mGlobalStartTimeNs) / 1000;
                        mMediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
                    }
                    mVideoCodec.releaseOutputBuffer(outputBufferId, false);
                }
            }
        }
    }

    private class AudioEncoderRunnable implements Runnable {
        @Override
        public void run() {
            mAudioCodec.start();
            mAudioRecord.startRecording();

            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            byte[] audioBuffer = new byte[AudioRecord.getMinBufferSize(
                    AUDIO_SAMPLE_RATE,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT)];

            while (mIsRecording) {
                int bytesRead = mAudioRecord.read(audioBuffer, 0, audioBuffer.length);
                if (bytesRead > 0) {
                    int inputBufferId = mAudioCodec.dequeueInputBuffer(10000);
                    if (inputBufferId >= 0) {
                        ByteBuffer inputBuffer = mAudioCodec.getInputBuffer(inputBufferId);
                        inputBuffer.clear();
                        inputBuffer.put(audioBuffer, 0, bytesRead);
                        // Sync timestamp to global start time
                        long presentationTimeUs = (System.nanoTime() - mGlobalStartTimeNs) / 1000;
                        mAudioCodec.queueInputBuffer(inputBufferId, 0, bytesRead, presentationTimeUs, 0);
                    }
                }

                int outputBufferId = mAudioCodec.dequeueOutputBuffer(bufferInfo, 10000);
                if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    // Add audio track to muxer once format is available
                    if (mAudioTrackIndex == -1) {
                        MediaFormat format = mAudioCodec.getOutputFormat();
                        mAudioTrackIndex = mMediaMuxer.addTrack(format);
                        startMuxerIfReady();
                    }
                } else if (outputBufferId >= 0) {
                    ByteBuffer outputBuffer = mAudioCodec.getOutputBuffer(outputBufferId);
                    if (bufferInfo.size > 0 && mMuxerStarted) {
                        mMediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
                    }
                    mAudioCodec.releaseOutputBuffer(outputBufferId, false);
                }
            }
        }
    }

    private void startMuxerIfReady() {
        // Start muxer only after both video and audio tracks are added
        if (mVideoTrackIndex != -1 && mAudioTrackIndex != -1 && !mMuxerStarted) {
            mMediaMuxer.start();
            mMuxerStarted = true;
        }
    }
}

Key Notes & Troubleshooting

  • Targeting Only SurfaceView: If you don't want to capture the entire screen, use mSurfaceView.getLocationOnScreen() to get its bounds, then adjust the VirtualDisplay width/height and position (you'll need to use MediaProjection.createVirtualDisplay() overload that accepts a display rectangle).
  • Time Sync: Using a shared global start time ensures video and audio timestamps are aligned, preventing desync issues.
  • Resource Cleanup: Always release codecs, MediaProjection, and VirtualDisplay in the order shown to avoid memory leaks or system crashes.
  • Production Error Handling: Add more robust error handling (e.g., catching codec configuration failures, handling muxer exceptions) for real-world use.
  • Android Version Support: This code works on Android 5.0 (API 21) and above, where MediaMuxer is stable.

内容的提问来源于stack exchange,提问作者TatiOverflow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:41:35