从广义上讲,编解码器就是处理输入数据来产生输出数据。MediaCode采用异步方式处理数据,并且使用了一组输入输出缓存(input and output buffers)。简单来讲,你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。
在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。
public void prepare(int width, int height) throws IOException {
// MIME_TYPE:"video/avc" -> H264 "video/hevc" -> H265
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
mWidth = width;
mHeight = height;
// Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
format.setInteger(MediaFormat.KEY_BIT_RATE, VideoConfig.BIT_RATE);
// FPS 每秒传输帧数(Frames Per Second)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
// I-frame 关键帧时间间隔,单位min
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
Log.d(TAG, "format: " + format);
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
// we can use for input and wrap it with a class that handles the EGL work.
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaFormat = mEncoder.getOutputFormat();
mEncoder.start();
}
如果从Camera拿到的数据为NV21/NV12格式,可以先通过 YUV库 将NV21/NV12转码为I420格式,再将数据送入编码器编码
/**
* 向编码器InputBuffer中填入数据
*
* @param data NV21数据
* @param timeSptamp 时间戳 ms
*/
private void putDataToInputBuffer(byte[] data, long timeSptamp) {
int index = mEncoder.dequeueInputBuffer(-1);
if (index >= 0) {
ByteBuffer buffer = mEncoder.getInputBuffer(index);
if (buffer == null) {
Log.d(TAG, "InputBuffer is null point");
return;
}
if (yuv == null) {
// YUV数据存储空间大小为 Y分量->width * height U、V分量->width * height / 4
yuv = new byte[mWidth * mHeight * 3 / 2];
}
// NV21格式数据转为I420P
nv21ToYuv420p(data, timeSptamp);
buffer.clear();
buffer.put(yuv);
mEncoder.queueInputBuffer(index, 0, data.length, timeSptamp * 1000, 0);
}
drainEncoder(false);
}
说明:视频添加文字/图片水印,可以在将YUV数据送入编码器前,将文字转为Bitmap,通过YUV库将ARGB转码为I420P,再使用YUV图片合成技术合成,这样编码后的H264/H265视频码流就添加上了水印。
/**
* 读取编码后的H264/H265数据
*
* @param endOfStream 标识是否结束
*/
public void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (endOfStream) {
Log.d(TAG, "sending EOS to encoder");
return;
}
while (true) {
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (!endOfStream) {
break; // out of while
} else {
Log.d(TAG, "no output available, spinning to await EOS");
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
mMediaFormat = mEncoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + mMediaFormat);
} else if (encoderStatus < 0) {
Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
encoderStatus);
// let's ignore it
} else {
ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus);
if (encodedData == null) {
Log.w(TAG, "encoderOutputBuffer " + encoderStatus +
" was null");
break;
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(mBufferInfo.offset);
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
// 取出编码好的H264数据
byte[] data = new byte[mBufferInfo.size];
encodedData.get(data);
// todo 将编码好的H264/H265数据存储到缓冲区或者传递给MediaMuxer生成视频文件(MP4)
// mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME 表示该帧数据为关键帧(I帧)
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
mBufferInfo.presentationTimeUs);
}
// 释放输出缓冲区
mEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.d(TAG, "reached end of stream unexpectedly");
} else {
Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。当处理这种压缩格式的数据时,这些数据必须在调用start()方法后且在处理任何帧数据之前提交给编解码器。这些数据必须在调用queueInputBuffer方法时使用BUFFER_FLAG_CODEC_CONFIG进行标记。
Codec-specific数据也可以被包含在传递给configure方法的格式信息(MediaFormat)中,在ByteBuffer条目中以"csd-0", "csd-1"等key标记。这些keys一直包含在通过MediaExtractor获得的Audio Track or Video Track的MediaFormat中。一旦调用start()方法,MediaFormat中的Codec-specific数据会自动提交给编解码器;你不能显示的提交这些数据。如果MediaFormat中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交codec-specific数据。在H264 AVC编码格式下,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。
Android 使用下列的codec-specific data buffers。对于适当的MediaMuxer轨道配置,这些也要在轨道格式中进行设置。每一个参数集以及被标记为(*)的codec-specific-data段必须以"\x00\x00\x00\x01"字符开头。
注意:当编解码器被立即刷新或start之后不久刷新,并且在任何输出buffer或输出格式变化被返回前需要特别地小心,因为编解码器的codec specific data可能会在flush过程中丢失。为保证编解码器的正常运行,你必须在刷新后使用标记为BUFFER_FLAG_CODEC_CONFIG的buffers再次提交这些数据。
调用start()或flush()方法后,输入数据在合适的流边界开始是非常重要的:其第一帧必须是关键帧(key-frame)。一个关键帧能够独立地完全解码(对于大多数编解码器它意味着I-frame),关键帧之后显示的帧不会引用关键帧之前的帧。
public void prepare(int width, int height, int fps, byte[] sps, byte[] pps) throws IOException {
String mimeType = "video/avc";
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
mWidth = width;
mHeight = height;
// 参见Codec-specific数据说明,H264数据格式需要 csd-0(sps)、csd-1(pps);
// H265数据格式需要 csd-0(vps+sps+pps)
if (sps != null) {
format.setByteBuffer("csd-0", ByteBuffer.wrap(sps));
}
if (pps != null) {
format.setByteBuffer("csd-1", ByteBuffer.wrap(pps));
}
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, 1);
Log.i(TAG, String.format("config codec:%s", format));
// 创建解码器
mDecoder = MediaCodec.createDecoderByType(mimeType);
// 配置解码器 format
mDecoder.configure(format, null, null, 0);
mDecoder.start();
}
注意:在解码H264/H265数据时,传递码流数据前一定要先配置好csd-*,参见Codec-specific说明。
/**
* 向解码器InputBuffer中填入数据
*
* @param data H264/H265数据
* @param timeSptamp 时间戳 us
*/
private void putDataToInputBuffer(byte[] data, long timeSptamp) {
int index = mDecoder.dequeueInputBuffer(-1);
if (index >= 0) {
ByteBuffer buffer = mDecoder.getInputBuffer(index);
if (buffer == null) {
LogUtils.d(TAG, "InputBuffer is null point");
return;
}
buffer.clear();
buffer.put(data);
Log.d(TAG, "queueInputBuffer data length: " + data.length + " timeSptamp: " + timeSptamp);
mDecoder.queueInputBuffer(index, 0, data.length, timeSptamp, 0);
}
drainDecoder(false, timeSptamp);
}
注意:传递给解码器的第一帧数据必须是关键帧(I-帧),参见流域界与关键帧说明。
/**
* 读取解码后的H264/H265数据
*
* @param endOfStream 标识是否结束
* @param timeSptamp 当前解码的数据的时间戳
*/
private void drainDecoder(boolean endOfStream, long timeSptamp) {
final int TIMEOUT_USEC = 10000;
if (endOfStream) {
Log.d(TAG, "sending EOS to encoder");
return;
}
while (true) {
int decoderStatus = mDecoder.dequeueOutputBuffer(mBufferInfo, 0);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet 输出为空
if (!endOfStream) {
break; // out of while
} else {
Log.d(TAG, "no output available, spinning to await EOS");
}
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
mMediaFormat = mDecoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + mMediaFormat);
} else if (decoderStatus < 0) {
Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
decoderStatus);
// let's ignore it
} else {
ByteBuffer decodedData = mDecoder.getOutputBuffer(decoderStatus);
if (decodedData == null) {
Log.w(TAG, "decoderOutputBuffer " + decoderStatus +
" was null");
break;
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
// adjust the ByteBuffer values to match BufferInfo (not needed?)
decodedData.position(mBufferInfo.offset);
decodedData.limit(mBufferInfo.offset + mBufferInfo.size);
// 取出解码好的NV12数据
byte[] data = new byte[mBufferInfo.size];
decodedData.get(data);
// todo 可以将解码后的NV12数据转码为ARGB8888,保存为jpg图片
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
mBufferInfo.presentationTimeUs);
}
mDecoder.releaseOutputBuffer(decoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.d(TAG, "reached end of stream unexpectedly");
} else {
Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
说明:解码后的NV12数据可以通过YUV库转码为ARGB8888格式,再将ARGB8888转为Bitmap对象,从而保存为jpeg格式的图片文件。
// 将ARGB8888原始数据转为Bitmap对象
Bitmap bitmap= Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(argb));
https://developer.android.google.cn/reference/android/media/MediaCodec
https://github.com/google/grafika
Android Camera预览时输出的帧率控制
https://chromium.googlesource.com/libyuv/libyuv/
使用libyuv对YUV数据进行缩放,旋转,镜像,裁剪等操作
YUV图像的水印的添加
EasyPlayer一款精炼、高效、稳定的流媒体播放器
H264(NAL简介与I帧判断)