求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
MediaProjectionto create aVirtualDisplaythat mirrors your SurfaceView (or adjust to target just the SurfaceView's area if needed). - Capture video frames from the
VirtualDisplayusing aSurfacethat feeds intoMediaCodecfor H.264 encoding. - Use
AudioRecordto capture raw microphone audio, then feed it into anotherMediaCodecfor AAC encoding. - Sync the encoded video and audio frames using a shared start timestamp, then write both to
MediaMuxerto 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 theVirtualDisplaywidth/height and position (you'll need to useMediaProjection.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
MediaMuxeris stable.
内容的提问来源于stack exchange,提问作者TatiOverflow
相关产品推荐
相关产品推荐

