这类的文章太多太多,也没时间整理,直接上部分核心源码和注意事项
1:MediaCodec核心类, 在往MediaCodec中不断的推数据时一定要使用BytePool字节数组池,MediaCodec编码后的byte[]可以循环重复使用,避免造成内存的抖动
/**
* Created by you on 2018-05-10.
* MediaCodec核心编码器
*/
public final class MediaEncoder implements Runnable {
//...伪代
//字节池
private final BytePool bytePool;
@Override
public void run() {
bufferInfo = new MediaCodec.BufferInfo();
callback.onInitStart();
while (isCoding.get()) {
try {
byte[] buffer = bufferQueue.take();
if (buffer == null || buffer.length == 0) {
break;//用空byte[]来终止循环与阻塞
}
codecDatas(buffer);
//缓存
bytePool.put(buffer);
} catch (InterruptedException e) {
if (!isCoding.get()) {
break;
}
}
}
release();
}
/**
* 编码datas数据
* @param buffer
*/
private void codecDatas(byte[] buffer) {
//加入缓冲区, -1如果当前没有可用的缓冲时会进入阻塞状态, 0时会立刻返回
int index = mediaCodec.dequeueInputBuffer(-1);
if (index >= 0) {
//填充数据
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(index);
inputBuffer.clear();
inputBuffer.put(buffer, 0, buffer.length);
callback.onEncodeInputBuffer(mediaCodec, buffer, index);
}
int encodeStatus;
while (true) {
//返回的三种状态 INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED, INFO_OUTPUT_BUFFERS_CHANGED,
encodeStatus = mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutUs);
if (encodeStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
break;//稍后重试
} else if (encodeStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//这里只会回调一次用于初始化
callback.onFormatChanged(mediaCodec);
} else if (encodeStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//忽略
} else {
//正常编码获得缓冲下标
ByteBuffer encodeData = mediaCodec.getOutputBuffer(encodeStatus);
//写入编码后的数据
callback.onWriteData(bufferInfo, encodeData);
//释放缓存冲,后续可以存放新的编码后的数据
mediaCodec.releaseOutputBuffer(encodeStatus, false);
}
}
}
}
2:使用OutputStream方式写入编码的h264(推荐使用MediaMuxer)
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
OutputStream.write(outData, 0, outData.length);
上面这种写法虽然方便但是也有错误,MediaCodec每次编码后的帧大小可能不一样,但是每次编码后都new byte也还是会造成内存抖动,需要采用如下方式,建一个适当大小的byte缓冲,用这个缓冲去接每一帧ByteBuffere中的数据再写入到OutputStream中, 也可以直接使用WritableByteChannel写入,原理其实差不多
//写入数据缓冲
private byte[] writeBuffer;
@Override
public void onWriteData(MediaCodec.BufferInfo bufferInfo, ByteBuffer encodeData) {
if (bufferInfo.size != 0) {
//将ByteBuffer中的数据写到文件中
// LogUtils.i("write buffinfosize %d", bufferInfo.size);
int offset = bufferInfo.offset;
int bufferSize = bufferInfo.size;
while (bufferSize > writeBuffer.length) {
writeByteBuffer(encodeData, offset, writeBuffer.length);
bufferSize -= writeBuffer.length;
offset += writeBuffer.length;
}
if (bufferSize > 0) {
writeByteBuffer(encodeData, offset, bufferSize);
}
//byte[] buf = new byte[bufferInfo.size];
//encodeData.get(buf); 不能用此种方式写入,内存抖动极大
}
}
/**
* 将ByteBuffer通过byte[]写入到文件
* @param encodeData
* @param offset
* @param length
*/
private void writeByteBuffer(ByteBuffer encodeData, int offset, int length) {
encodeData.position(offset);
encodeData.limit(offset + length);
encodeData.get(writeBuffer, 0, length);
try {
bos.write(writeBuffer, 0, length);
} catch (IOException e) {
e.printStackTrace();
}
}
3:使用OutputStream方式写入编码后aac文件(推荐使用MediaMuxer),同h264一样需要一个byte缓冲去接ByteBuffere中的数据再写入到OutputStream中
aac压缩格式可以直接使用播放器播放,采用 ADTS 格式需要给每帧加上 7 个字节的头信息.(MediaMuxer会自动处理)
@Override
public void onWriteData(MediaCodec.BufferInfo bufferInfo, ByteBuffer encodeData) {
if (bufferInfo.size != 0) {
encodeData.position(bufferInfo.offset);
encodeData.limit(bufferInfo.offset + bufferInfo.size);
addADTStoPacket(bufferInfo.size + 7);
try {
bos.write(adtsHeader, 0, 7);
} catch (IOException e) {
e.printStackTrace();
}
//将ByteBuffer中的数据写到文件中
LogUtils.i("write buffinfosize %d", bufferInfo.size);
int offset = bufferInfo.offset;
int bufferSize = bufferInfo.size;
while (bufferSize > writeBuffer.length) {
writeByteBuffer(encodeData, offset, writeBuffer.length);
bufferSize -= writeBuffer.length;
offset += writeBuffer.length;
}
if (bufferSize > 0) {
writeByteBuffer(encodeData, offset, bufferSize);
}
//byte[] buf = new byte[bufferInfo.size];
//encodeData.get(buf); 不能用此种方式写入,内存抖动极大
}
}
private void addADTStoPacket(int packetLen) {
adtsHeader[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
adtsHeader[4] = (byte) ((packetLen & 0x7FF) >> 3);
adtsHeader[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
adtsHeader[6] = (byte) 0xFC;
}
4:MediaMuxer混合录制,需要aac与h264都有MediaCodec回调INFO_OUTPUT_FORMAT_CHANGED状态时添加mediaMuxer.addTrack(mediaCodec.getOutputFormat());才可以开启MediaMuxer, 结束亦是如此,否则会抛异常
会用到多线程之间的操作,最先mediaMuxer.addTrack的进入wait()等侍状态,最后一个mediaMuxer.addTrack的notifyAll()
/**
* avc与aac同时都已addTrack时才可开启
*/
private synchronized void startMuxer() {
if (!isMuxerStarted && isRecording) {
if (audioTrackIndex != -1 && h264TrackIndex != -1) {
mediaMuxer.start();
isMuxerStarted = true;
//最后一个addTrack的来开启
notifyAll();
} else {
long c = System.currentTimeMillis();
do {
try {
//先addTrack的处于等侍状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (isRecording && (audioTrackIndex == -1 || h264TrackIndex == -1));
long n = System.currentTimeMillis() - c;
LogUtils.i("wait... %d", n);
}
}
}
/**
* aac, h264编码都停止时才可停止
*/
private synchronized void stopMuxer() {
if (isMuxerStarted && h264Released && audioReleased) {
mediaMuxer.stop();
mediaMuxer.release();
isMuxerStarted = false;
mediaMuxer = null;
LogUtils.i("mp4recorder release...");
}
}
5:注意在MediaCodec编码aac时,需要对音频进行时间采样计算,否则在混合的时候容易抛出类似
MPEG4Writer: timestampUs 6220411 < lastTimestampUs 6220442 for Audio track异常,报异常原因也很简单,当aac编码通过mediaMuxer.addTrack()进入等侍wait状态等侍h264同步进行时,等侍的时间超过了差值6220442微秒, 另外在视频H264与PCM解码播放时,一般也都是根据pcm的播放速度来做同步的,因此音频的时间采样非常非常重要
public final class AudioPresentationTime {
private long startTime;
private final long bufferDurationUs;
private long currentCount;
/**
*
* @param bufferSize
* @param sampleRate
* @param channelCount
* @param audioFormat
*/
public AudioPresentationTime(int bufferSize, int sampleRate, int channelCount, int audioFormat) {
int bitByteSize = audioFormat == AudioFormat.ENCODING_PCM_16BIT ? 2 : 1; //16bit = 2 byte
bufferDurationUs = 1_000_000L * (bufferSize / (channelCount * bitByteSize)) / sampleRate;
}
public void start() {
startTime = System.nanoTime() / 1000L;
currentCount = 0;
}
public long getPresentationTimeUs() {
return currentCount++ * bufferDurationUs + startTime;
}
}
补充:除了要对对音频进行时间采样计算外,H264编码的Camera采集数据也需要注意预览的尺寸大小和帧率的控制, 在预览尺寸过大或者帧率过高的时候,MediaCodec编码的速度赶不上采集的速度(预览回调的YUV数据越大编码耗时越大),就会造成等侍编码的YUV数据队列的爆满内存溢出,或者在有限制队列大小时也会造成中间帧的丢失.