Android Camera系列(六):MediaCodec视频编码上-编码YUV
己所不欲勿施于人
-
Android Camera系列(一):SurfaceView+Camera
-
Android Camera系列(二):TextureView+Camera
-
Android Camera系列(三):GLSurfaceView+Camera
-
Android Camera系列(四):TextureView+OpenGL ES+Camera
-
Android Camera系列(五):Camera2
-
Android Camera系列(六):MediaCodec视频编码上-编码YUV
本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。
引言
前面我们已经花费了大量的章节来讲解Camera配合各种View用来预览、拍照等,接下来我们来讲解如何进行视频录制。Android中要进行录制视频有很多种方式,如:使用系统相机录制、MediaRecorder+Camera自定义录制等。如果上面的方式能够满足你的需求,那本章你可以划走了。
假如有这么一种需求,我们要获取Camera的帧数据流YUV进行实时检测识别,同时我又要将检测到的数据进行保存,那么MediaCodec就很合适,MediaCodec(硬编解码器)主要对帧数据进行编解码处理。实际的例子就好比抖音拍摄视频,人实时美颜或贴图后将视频保存。
除了可以使用MediaCodec对数据流进行编解码,还有ffmpeg同样也可以。
- MediaCodec属于硬解码,处理数据使用GPU效率高,功能受硬件限制兼容性略低。
- FFmpeg属于软解码,处理数据使用CPU效率低,功能强大兼容性好。当然该方式不在我们本章讨论范围
MediaCodec介绍
MediaCodec是Android平台上的一个多媒体编解码器,它可以用于对音频和视频进行编解码。通过MediaCodec,开发者可以直接访问底层的编解码器,实现更高效的音视频处理。同时,MediaCodec也支持硬件加速,可以利用设备的硬件资源来提高编解码的性能。MediaCodec主要应用于以下几个方面:
- 音视频编解码:MediaCodec可以对音频和视频进行硬件加速的编解码处理,可以实现高效的音视频处理和播放。
- 多媒体格式支持:支持常见的音视频格式,包括H.264、AAC、MP3等,可以进行解码和编码操作。
- 硬件加速:利用设备的硬件加速功能,可以提高音视频处理的效率和性能。
- 实时处理:支持实时的音视频处理,适用于实时通信、直播等场景。
- 自定义处理:可以通过MediaCodec进行自定义的音视频处理,如滤镜、特效等操作。
MediaCodec在Android平台上提供了强大的音视频编解码功能,可以用于多媒体应用的开发和优化。
硬编码流程
- 使用
MediaCodec
,将yuv数据编码获得h264数据 MediaMuxer
用来封装h264获得一个mp4文件
MediaCodec使用
1. 创建MediaCodec
通过静态方法创建:createDecoderByType
、createEncoderByType
、createByCodecName
使用 createDecoderByType
、createEncoderByType
两个,创建用于解码和编码的实体,传入参数为编解码器的mime type名称。createByCodecName
需要传入具体编解码器的名称。
官方建议:最好使用MediaCodecList.findEncoderForFormat和createByCodecName来确保生成的编解码器可以处理给定的格式。
mime可用的类型:
* "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
* "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
* "video/avc" - H.264/AVC video
* "video/hevc" - H.265/HEVC video
* "video/mp4v-es" - MPEG4 video
* "video/3gpp" - H.263 video
* "audio/3gpp" - AMR narrowband audio
* "audio/amr-wb" - AMR wideband audio
* "audio/mpeg" - MPEG1/2 audio layer III
* "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
* "audio/vorbis" - vorbis audio
* "audio/g711-alaw" - G.711 alaw audio
* "audio/g711-mlaw" - G.711 ulaw audio
最常用的是 : “video/avc” - H.264/AVC video,现在网络上流传的短视频的视轨格式基本上都是H264。
private static final String MIME_TYPE = "video/avc";
// 创建MediaCodec
MediaCodec mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
2. 配置MediaCodec
实例化MediaCodec后,我们需要配置一些参数如:视频录制的宽高、码率、帧率等信息。配置使用的类是MediaFormat
调用MediaCodec
的configure
方法:
public void configure(
MediaFormat format,
Surface surface,
MediaCrypto crypto,
int flags
);
- MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。
- Surface surface:指定surface,一般用于解码器,设置后解码的内容会被渲染到所指定的surface上。无需要则传null
- MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
- int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
后面三个参数基本是固定的,基本不需要变化。
第一个参数MediaFormat是需要我们深入去了解下,解码的情况下,该参数大多通过MediaExtractor从视频中获取,这里先不讨论。编码的情况实体需要由我们来进行创建,常用的方法是它的静态方法:createVideoFormat
、createAudioFormat
。
以视频为例:
MediaFormat format = MediaFormat.createVideoFormat(MIME,width, height);
- MIME:第一步中的mine_type
- width,height:视频的分辨率/高宽
MediaFormat 必须设置的参数:YUV编码格式、比特率、帧率、关键帧间隔,不设置会报异常。
// 设置编码yuv格式
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
// 设置比特率
format.setInteger(MediaFormat.KEY_BIT_RATE, 1 * 1024 * 1024);
// 设置帧率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
// 设置关键帧间隔,单位:秒
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
需要特別说明的是MediaFormat.KEY_COLOR_FORMAT :该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。
比如:Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。
MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:
// 最常用的
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, // 19, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, // 21, NV12,测试通过
// 其他不常用
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, // 20, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, // 39, NV12, 测试通过
YUV各种格式还不熟悉的,请看下面的说明:
- YUV420p: I420、YV12
- YUV420sp: NV12、NV21
同样,对于一个6*4的图像,这四种像素格式的存储方式如下:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
U U U U U U V V V V V V U V U V U V V U V U V U
V V V V V V U U U U U U U V U V U V V U V U V U
- I420 - - YV12 - - NV12 - - NV21 -
虽然MediaCodec支持了多种YUV编码格式,而且代码中也有对应的值,但是正如我们引言中说的MediaCodec兼容性不好。支持哪种硬编码格式需要根据硬件来定,这也是大多数初入MediaCodec的人来说最头疼的地方。
3. 选择合适的编码器
MediaCodec都知道是硬编解码器,其实不然。他也是支持软编解码的,软编解码由Google实现,硬编解码由厂商自定义。这也就是兼容性的根源,因为我们不知道厂商是否真的支持硬编解码。我们只能自己查找合适的编解码器。而每种编解码器支持编码的YUV格式又不尽相同,我们是优先选择编码器还是优先选择YUV格式这是我们需要抉择的。
看过大部分的博文讲解都是优先选择编码器,而一旦我们选择的编码器后,就只能使用编码器中支持的YUV格式进行编码,如果发现YUV格式不是我们想要的,那么就会编码失败。而我们这里优先根据YUV格式来选择编码器,这么做的理由是有更好的兼容性,但是可能会牺牲一部分性能。
public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder {
private static final String MIME_TYPE = "video/avc";
protected int mColorFormat;
...
/**
* select the first codec that match a specific MIME type
*
* @param mimeType
* @return null if no codec matched
*/
@SuppressWarnings("deprecation")
protected final MediaCodecInfo selectVideoCodec(final String mimeType) {
if (DEBUG) Log.v(TAG, "selectVideoCodec:");
// get the list of available codecs
final int numCodecs = MediaCodecList.getCodecCount();
// 硬编码器列表
List<MediaCodecInfo> hardwareCodecInfoList = new ArrayList<>();
// 软编码器列表
List<MediaCodecInfo> softwareCodecInfoList = new ArrayList<>();
for (int i = 0; i < numCodecs; i++) {
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) { // skipp decoder
continue;
}
/**
* 硬件编解码器
* OMX.qcom.video.encoder.heic
* OMX.qcom.video.decoder.avc
* OMX.qcom.video.decoder.avc.secure
* OMX.qcom.video.decoder.mpeg2
* OMX.google.gsm.decoder
* OMX.qti.video.decoder.h263sw
* c2.qti.avc.decoder
*
* 软件编解码器,通常以OMX.google或者c2.android开头
* OMX.google.h264.encoder
* c2.android.aac.decoder
* c2.android.aac.decoder
* c2.android.aac.encoder
* c2.android.aac.encoder
* c2.android.amrnb.decoder
* c2.android.amrnb.decoder
*/
// select first codec that match a specific MIME type and color format
final String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// if (codecInfo.getName().contains("qcom") || codecInfo.getName().contains("c2.qti")|| codecInfo.getName().contains("c2.android")) continue;
if (codecInfo.isVendor()) { // 硬解码
hardwareCodecInfoList.add(codecInfo);
} else { // 软解码
softwareCodecInfoList.add(codecInfo);
}
} else {
if (codecInfo.getName().contains("google") || codecInfo.getName().startsWith("c2.android")) { // 软解码
softwareCodecInfoList.add(codecInfo);
} else { // 硬解码
hardwareCodecInfoList.add(codecInfo);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Logs.i(TAG, "codec:" + codecInfo.getName() + ", " + codecInfo.isVendor() + ", " + getSupportColorFormatStr(codecInfo, mimeType));
} else {
Logs.i(TAG, "codec:" + codecInfo.getName() + ", " + getSupportColorFormatStr(codecInfo, mimeType));
}
}
}
}
for (int i = 0; i < recognizedFormats.length; i++) {
int colorFormat = recognizedFormats[i];
for (MediaCodecInfo codecInfo : hardwareCodecInfoList) {
if (containsColorFormatByCodec(colorFormat, codecInfo, mimeType)) {
mColorFormat = colorFormat;
return codecInfo;
}
}
}
for (int i = 0; i < recognizedFormats.length; i++) {
int colorFormat = recognizedFormats[i];
for (MediaCodecInfo codecInfo : softwareCodecInfoList) {
if (containsColorFormatByCodec(colorFormat, codecInfo, mimeType)) {
mColorFormat = colorFormat;
return codecInfo;
}
}
}
return null;
}
protected static final boolean containsColorFormatByCodec(int requestColorFormat, final MediaCodecInfo codecInfo, final String mimeType) {
final MediaCodecInfo.CodecCapabilities caps;
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
caps = codecInfo.getCapabilitiesForType(mimeType);
} finally {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
}
for (int i = 0; i < caps.colorFormats.length; i++) {
if (requestColorFormat == caps.colorFormats[i]) {
return true;
}
}
return false;
}
protected static final String getSupportColorFormatStr(final MediaCodecInfo codecInfo, final String mimeType) {
final MediaCodecInfo.CodecCapabilities caps;
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
caps = codecInfo.getCapabilitiesForType(mimeType);
} finally {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
}
StringBuffer sb = new StringBuffer();
sb.append("[");
for (int i = 0; i < caps.colorFormats.length; i++) {
sb.append(caps.colorFormats[i]);
if (i == caps.colorFormats.length - 1) {
sb.append("]");
} else {
sb.append(", ");
}
}
return sb.toString();
}
/**
* color formats that we can use in this class
*/
protected static int[] recognizedFormats;
/**
A5. The color formats for the camera output and the MediaCodec encoder input are different. Camera supports YV12 (planar YUV 4:2:0) and NV21 (semi-planar YUV 4:2:0). The MediaCodec encoders support one or more of:
#19 COLOR_FormatYUV420Planar (I420)
#20 COLOR_FormatYUV420PackedPlanar (also I420)
#21 COLOR_FormatYUV420SemiPlanar (NV12)
#39 COLOR_FormatYUV420PackedSemiPlanar (also NV12)
#0x7f000100 COLOR_TI_FormatYUV420PackedSemiPlanar (also also NV12)
*/
static {
recognizedFormats = new int[]{
// 最常用的
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, // 19, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, // 21, NV12,测试通过
// 其他不常用
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, // 20, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, // 39, NV12, 测试通过
// MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, // 2141391872, NV21, 测试通过,该格式部分设备录制绿屏(不支持部分分辨率的编解码)
// MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar, // 2130706688, NV12, 未测试
};
}
}
上面代码的逻辑是优先从最常用的YUV格式COLOR_FormatYUV420Planar
、COLOR_FormatYUV420SemiPlanar
、COLOR_FormatYUV420PackedPlanar
、COLOR_FormatYUV420PackedSemiPlanar
选择,如果硬编码器中有这些格式我们选择硬编码器,如果硬编码器没有这几种格式,那么我就从软编码器中选择。
如果你发现这四种YUV编码格式兼容性还不足够好,那么我们就选择最常用的两种
4. 启动编码器
当我们选择好合适的编码器,配置好视频参数后,我们就可以启动MediaCodec了,核心代码如下:
public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder {
private static final boolean DEBUG = Logs.issIsLogEnabled(); // TODO set false on release
private static final String TAG = "MediaVideoBufferEncoder";
private static final String MIME_TYPE = "video/avc";
// parameters for recording
private static final int FRAME_RATE = 15;
private static float BPP = 1.0f;
private final int mWidth, mHeight;
protected int mColorFormat;
@Override
protected void prepare() throws IOException {
if (DEBUG) Log.i(TAG, "prepare: ");
mTrackIndex = -1;
mMuxerStarted = mIsEOS = false;
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE);
if (videoCodecInfo == null) {
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Logs.i(TAG, "codec:" + videoCodecInfo.getName() + ", " + videoCodecInfo.isVendor() + ", " + getSupportColorFormatStr(videoCodecInfo, MIME_TYPE));
} else {
Logs.i(TAG, "codec:" + videoCodecInfo.getName() + ", " + getSupportColorFormatStr(videoCodecInfo, MIME_TYPE));
}
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName());
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate());
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 描述编码器要使用的所需比特率模式的键
// BITRATE_MODE_CQ: 表示完全不控制码率,尽最大可能保证图像质量
// BITRATE_MODE_CBR: 表示编码器会尽量把输出码率控制为设定值
// BITRATE_MODE_VBR: 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;
boolean isBitrateModeSupported = videoCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC).getEncoderCapabilities().isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
Logs.i(TAG, "isBitrateModeSupported:" + isBitrateModeSupported);
if (isBitrateModeSupported) {
format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
}
}
if (DEBUG) Log.i(TAG, "format: " + format);
mMediaCodec = MediaCodec.createByCodecName(videoCodecInfo.getName());
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
if (DEBUG) Log.i(TAG, "prepare finishing");
if (mListener != null) {
mListener.onPrepared(this);
}
}
}
5. 编码数据
数据编码流程如上图,input 为输入端,output 为输出端,输入和输出端各有若干个 buffer,输入端不断拿到一个空 buffer,装上数据,再传入 MediaCodec 直到所有数据输入为止,输出端不断从 MediaCodec 获取到 buffer,每次得到处理好的数据后,再将 buffer 交还给 MediaCodec。
关键步骤:
- 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0);//获取输入缓存区授权
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(…);//获取实际buffer
// 填充
...
mMediaCodec.queueInputBuffer(inputBufferId, …);//加入编码队列
核心代码如下:
protected void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) {
if (!mIsCapturing) return;
int ix = 0, sz;
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
while (mIsCapturing && ix < length) {
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
if (inputBufferIndex >= 0) {
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
sz = inputBuffer.remaining();
sz = (ix + sz < length) ? sz : length - ix;
if (sz > 0 && (buffer != null)) {
buffer.position(ix + sz);
buffer.flip();
inputBuffer.put(buffer);
}
ix += sz;
if (length <= 0) {
// send EOS
mIsEOS = true;
if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM");
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0,
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
break;
} else {
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz,
presentationTimeUs, 0);
}
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// wait for MediaCodec encoder is ready to encode
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
// will wait for maximum TIMEOUT_USEC(10msec) on each call
}
}
}
- MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。
int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);//获取输出缓存区授权
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);//获取实际buffer取得编码后的数据
。。。
codec.releaseOutputBuffer(outputBufferId, …);//释放使缓冲buffer回到队列中
- 将编码好的h264数据使用
MediaMuxer
封装成mp4文件
核心代码如下:
protected void drain() {
if (mMediaCodec == null) return;
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
int encoderStatus, count = 0;
final MediaMuxerWrapper muxer = mWeakMuxer.get();
if (muxer == null) {
// throw new NullPointerException("muxer is unexpectedly null");
Log.w(TAG, "muxer is unexpectedly null");
return;
}
LOOP:
while (mIsCapturing) {
// get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
if (!mIsEOS) {
if (++count > 5)
break LOOP; // out of while
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
// this shoud not come when encoding
encoderOutputBuffers = mMediaCodec.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED");
// this status indicate the output format of codec is changed
// this should come only once before actual encoded data
// but this status never come on Android4.3 or less
// and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
if (mMuxerStarted) { // second time request is error
throw new RuntimeException("format changed twice");
}
// get output format from codec and pass them to muxer
// getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
mTrackIndex = muxer.addTrack(format);
mMuxerStarted = true;
if (!muxer.start()) {
// we should wait until muxer is ready
synchronized (muxer) {
while (!muxer.isStarted())
try {
muxer.wait(100);
} catch (final InterruptedException e) {
break LOOP;
}
}
}
} else if (encoderStatus < 0) {
// unexpected status
if (DEBUG)
Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus);
} else {
final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
// this never should come...may be a MediaCodec internal error
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// You shoud set output format to muxer here when you target Android4.3 or less
// but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
// therefor we should expand and prepare output format from buffer data.
// This sample is for API>=18(>=Android 4.3), just ignore this flag here
if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
// encoded data is ready, clear waiting counter
count = 0;
if (!mMuxerStarted) {
// muxer is not ready...this will prrograming failure.
throw new RuntimeException("drain:muxer hasn't started");
}
// write encoded data to muxer(need to adjust presentationTimeUs.
mBufferInfo.presentationTimeUs = getPTSUs();
muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
prevOutputPTSUs = mBufferInfo.presentationTimeUs;
}
// return buffer to encoder
mMediaCodec.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// when EOS come.
mMuxerStarted = mIsCapturing = false;
break; // out of while
}
}
}
}
dequeueOutputBuffer的 mBufferInfo 为入参(传入一个实体,函数内会为这个实体的字段赋值),其中有buffer的size,offsize信息,帧的时间戳信息和用于表示帧属性的flag字段(最常用的就是用来判断是否是关键帧)。
- 循环上面的步骤,当编码完成时,在步骤1传入
MediaCodec.BUFFER_FLAG_END_OF_STREAM
标识,用以标志整个流程的结束。
mMediaCodec.queueInputBuffer( videoInputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
在步骤2中读取到MediaCodec.BUFFER_FLAG_END_OF_STREAM时就可以停止整个编码任务。
- 对YUV数据编码,我们使用前面的篇章从
PreviewBufferCallback
中获取NV21数据就可以进行编码生成MP4了
从回调中获取NV21数据
private PreviewBufferCallback mPreviewBufferCallback = new PreviewBufferCallback() {
@Override
public void onPreviewBufferFrame(byte[] data, int width, int height, YUVFormat format) {
if (mEncoder != null) {
mEncoder.encode(data);
}
}
};
根据编码器支持的格式对Camera数据进行转换后编码
/**
* 编码数据
*
* @param data nv21
*/
public void encode(byte[] data) {
encode(data, YUVFormat.NV21);
}
/**
* 编码数据
*
* @param data YUV420
*/
public void encode(byte[] data, YUVFormat yuvFormat) {
if (mEncoder != null) {
int mColorFormat = mEncoder.getColorFormat();
byte[] encodeData = null;
long start = System.currentTimeMillis();
if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
|| mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar) { // 19, 20:I420
if (yuvFormat == YUVFormat.NV21) {
YUVUtils.nativeNV21ToI420(data, mSize.getWidth(), mSize.getHeight(), mTempData);
encodeData = mTempData;
} else {
encodeData = data;
}
} else if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|| mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar
|| mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar) { // 21, 39:NV12
// 使用C层转换最快
if (yuvFormat == YUVFormat.NV21) {
YUVUtils.nativeNV21ToNV12(data, mSize.getWidth(), mSize.getHeight(), mTempData);
} else {
YUVUtils.nativeI420ToNV12(data, mSize.getWidth(), mSize.getHeight(), mTempData);
}
encodeData = mTempData;
} else if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar) {// 2141391872:NV21
if (yuvFormat == YUVFormat.NV21) {
encodeData = data;
} else {
YUVUtils.nativeI420ToNV21(data, mSize.getWidth(), mSize.getHeight(), mTempData);
encodeData = mTempData;
}
}
mEncoder.frameAvailableSoon();
ByteBuffer buffer = ByteBuffer.wrap(encodeData);
mEncoder.encode(buffer);
}
}
以上只是关键代码,看详细代码去git中查看
MediaEncoder
类即可
6. 释放编码器
调用MediaCodec的stop
和release
方法
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
再谈兼容性
文章处理了MediaCodec一些兼容性问题,但是还不够。事物的运行总是不按我们预想的来,COLOR_FormatYUV420SemiPlanar
我们测试了好多机型都是NV12
格式,但还是有少部分机型需要NV21
,这让我们的适配变得困难。如果这种问题上了生产,大量用户使用,那么这个小问题就会被放大。
如果我们的硬件设备固定,MediaCodec是可以使用。如果是ToC,我这里还是建议慎重。
我们大篇幅讲解了MediaCodec难道真的只能用来纸上谈兵了吗???
答案是否定的,MediaCodec除了可以使用YUV数据进行编码,还支持COLOR_FormatSurface
的方式进行编码。该方式通过MediaCodec创建Surface,然后Camera通过OpenGL把数据渲染到该Surface上,就可以进行编码。前面的篇章我们已经通过OpenGL将Camera数据渲染到视图Surface中,那现在要渲染到视频Surface中其实也是很简单的事情。该种方式兼容性好(Android > 4.3即可),使用OpenGL效率更高,我们会在接下来的篇章中详细介绍。
最后
lib-camera库包结构如下:
包 | 说明 |
---|---|
camera | camera相关操作功能包,包括Camera和Camera2。以及各种预览视图 |
encoder | MediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制 |
gles | opengles操作相关 |
permission | 权限相关 |
util | 工具类 |
每个包都可独立使用做到最低的耦合,方便白嫖
github地址:https://github.com/xiaozhi003/AndroidCamera
参考:
- https://github.com/saki4510t/UVCCamera
- https://github.com/google/grafika