一.前言
在 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<byte[]> 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 (mIsEncodin