一.前言
在 AAC 音频编码保存和解码播放 和 Camera 视频采集,H264 编码保存两篇文章中介绍了如何通过 AudioRecord 和 MediaCodec 录制 AAC 音频以及如何通过 Camera 和 MediaCodec 录制 H264 视频。本文将介绍如何通过 MediaMuxer 合成 MP4 文件。
MP4
在 音视频开发基础概念 中有介绍过,MP4 (或者称 MPEG-4) 是一种标准的数字多媒体容器格式,可以存储 音频数据和视频数据。对于视频格式,常见的是 H264 和 H265; 对于音频格式通常是 AAC 。
MediaMuxer
MediaMuxer 是 Android 用来产生一个混合音频和视频多媒体文件的 API ,只支持下面几种格式。
public static final inMUXER_OUTPUT_3GPP = 2;public static final inMUXER_OUTPUT_HEIF = 3;public static final inMUXER_OUTPUT_MPEG_4 = 0;public static final inMUXER_OUTPUT_OGG = 4;public static final inMUXER_OUTPUT_WEBM = 1;
1. 初始化
mMediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
path 表示 MP4 文件的输出路径
2. 添加音频轨和视频轨
if (type == AAC_ENCODER) {
mAudioTrackIndex = mMediaMuxer.addTrack(mediaFormat);}if (type == H264_ENCODER) {
mVideoTrackIndex = mMediaMuxer.addTrack(mediaFormat);}
传入 MediaFormat 对象从 MediaCodec 中获取。
3. 开始合成
mMediaMuxer.start();
4. 写入数据
mMediaMuxer.writeSampleData(avData.trackIndex, avData.byteBuffer, avData.bufferInfo);
5. 停止并释放资源
mMediaMuxer.stop();mMediaMuxer.release();
二. 录制 MP4
AudioTrack、Camera、MediaCodec 和 MediaMuxer 录制 MP4 流程如下图所示:
1. 音频录制
音频录制使用 AudioRecord
public class AudioRecorder {
private int mAudioSource; private int mSampleRateInHz; private int mChannelConfig; private int mAudioFormat; private int mBufferSizeInBytes; private AudioRecord mAudioRecord; private volatile boolean mIsRecording; private Callback mCallback; private byte[] mBuffer; public void setCallback(Callback callback) {
mCallback = callback; } public interface Callback {
void onAudioOutput(byte[] data); } public AudioRecorder(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes) {
mAudioSource = audioSource; mSampleRateInHz = sampleRateInHz; mChannelConfig = channelConfig; mAudioFormat = audioFormat; mBufferSizeInBytes = bufferSizeInBytes; mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes); mIsRecording = false; int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT); mBuffer = new byte[Math.min(2048, minBufferSize)]; } public void start() {
if (mIsRecording) {
return; } new Thread(new Runnable() {
@Override public void run() {
onStart(); } }).start(); } public void onStart() {
if (mAudioRecord == null) {
mAudioRecord = new android.media.AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig, mAudioFormat, mBufferSizeInBytes); } mAudioRecord.startRecording(); mIsRecording = true; while (mIsRecording) {
int len = mAudioRecord.read(mBuffer, 0, mBuffer.length); if (len > 0) {
if (mCallback != null) {
mCallback.onAudioOutput(mBuffer); } } } mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; } public void stop() {
mIsRecording = false; }}
2. 音频编码
音频编码使用阻塞队列 BlockingQueue 来缓冲数据,编码成 AAC 格式。
public class AacEncoder {
public static final int AAC_ENCODER = 2; private MediaCodec mAudioEncoder; private MediaFormat mMediaFormat; private BlockingQueue mDataQueue; private volatile boolean mIsEncoding; private Callback mCallback; private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";//就是 aac public void setCallback(Callback callback) {
mCallback = callback; } public interface Callback {
void outputMediaFormat(int type, MediaFormat mediaFormat); void onEncodeOutput(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo); void onStop(int type); } public AacEncoder(int sampleRateInHz, int channelConfig, int bufferSizeInBytes) {
try {
mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); mMediaFormat = MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, sampleRateInHz, channelConfig == AudioFormat.CHANNEL_OUT_MONO ? 1 : 2); mMediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); mMediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_STEREO);//CHANNEL_IN_STEREO 立体声 int bitRate = sampleRateInHz * 16 * channelConfig == AudioFormat.CHANNEL_IN_MONO ? 1 : 2; mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); mMediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelConfig == AudioFormat.CHANNEL_IN_MONO ? 1 : 2); mMediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRateInHz); mAudioEncoder.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (IOException e) {
} mDataQueue = new ArrayBlockingQueue<>(10); mIsEncoding = false; } public void start() {
if (mIsEncoding) {
return; } new Thread(new Runnable() {
@Override public void run() {
onStart(); } }).start(); } public void stop() {
mIsEncoding = false; } private void onStart() {
mIsEncoding = true; mAudioEncoder.start(); byte[] pcmData; int inputIndex; ByteBuffer inputBuffer; ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers(); int outputIndex; ByteBuffer outputBuffer; ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (mIsEncoding || !mDataQueue.isEmpty()) {
pcmData = dequeueData(); if (pcmData == null) {
cont