在上篇文章,我们已经成功的满足了需求,在预览摄像头的同时加上一些简单的视频二次处理(水印)。接下来我们就是要把视频录制下来,这就涉及视频的编码范畴了。视频编解码知识点无论在哪个平台上的操作系统上,都是比较难的一个知识点。在Android 4.1以前,Android并没有提供硬编硬解的API,所以之前基本上都是采用FFMpeg来做视频软件编解码的,现在FFMpeg在Android的编解码上依旧广泛应用。通常来说,对于同一平台同一硬件环境,硬编硬解的速度是快于软件编解码的。而且相比软件编解码的高CPU占用率来说,硬件编解码也有很大的优势,所以在硬件支持的情况下,一般硬件编解码是我们的首选。 本篇博客主要是利用Android4.1增加的API MediaCodec和Android 4.3增加的API MediaMuxer进行Mp4视频的录制。
https://developer.android.com/reference/android/media/MediaCodec
既然需要使用系统API进行硬编码录制视频,我们就从官方文档入手(上方连接,需要梯子)看看MediaCodec是怎么玩的。
官网上的图能够很好的说明MediaCodec的使用方式。我们从这两段英文入手理解MediaCodec:
MediaCodec类可用于访问低层媒体编解码器,即编码器/解码器组件。它是Android低层多媒体支持基础设施的一部分。(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.一起联合使用)
广义地说,编解码器处理输入数据以产生输出数据。它异步处理数据,并使用一组输入和输出缓冲区。在一个简单化的层次上,你请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。编解码器使用的数据,并将其转换为其空输出缓冲区之一。最后,请求(或接收)填充的输出缓冲区,消耗其内容并将其释放回编解码器。
好了基本意思就是这样了,我们可以看到,工作原理比较简单,其中有几个关键字:异步(线程工作),编解码(不单指是流转文件,也可以文件转流,或者更实际的网络拉流直播显示),媒体数据(不单只是视频还可以音频)。
废话不多,但我还是想要废话几句的,就是先看看官方的MediaCodec和MediaMuxer的Sample。我不怎么喜欢对API的介绍因为这些网上太多了,而且都一样,稍微有个理解认识就可以了。
public class CameraRecordEncoderCore {
private static final String TAG = "CameraRecordEncoderCore";
private static final boolean DEBUG = true;
private static final int FRAME_RATE = 30; // 30fps
private static final int I_FRAME_INTERVAL = 5; // I-frames 间隔 5s
private MediaCodec mVideoEncoder;
private Surface mInputSurface;
private MediaMuxer mMuxer;
/**
* 配置 编码器和合成器的各种状态,准备输入源供外部喂养数据。
* @param width 编码视频的宽度
* @param height 编码视频的高度
* @param bitRate 比特率/码率
* @param outputFile 输出mp4路径
*/
public CameraRecordEncoderCore(int width, int height, int bitRate, File outputFile)
throws IOException {
// 1. 设置编码器类型
// MediaFormat.MIMETYPE_VIDEO_AVC = "video/avc"; // H.264 Advanced Video Coding
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, //设置输入源类型为原生Surface 重点1 参考下面官网复制过来的说明
COLOR_FormatSurface);
//Raw Video Buffers
// In ByteBuffer mode video buffers are laid out according to their color format.
// You can get the supported color formats as an array from getCodecInfo().getCapabilitiesForType(…).colorFormats.
// Video codecs may support three kinds of color formats:
// I、native raw video format: This is marked by COLOR_FormatSurface and
// it can be used with an input or output Surface.
// II、flexible YUV buffers (such as COLOR_FormatYUV420Flexible): These can be used with an input/output Surface,
// as well as in ByteBuffer mode, by using getInput/OutputImage(int).
// III、other, specific formats: These are normally only supported in ByteBuffer mode.
// Some color formats are vendor specific. Others are defined in MediaCodecInfo.CodecCapabilities.
// For color formats that are equivalent to a flexible format, you can still use getInput/OutputImage(int).
// 2. 创建我们的编码器,配置我们以上的设置
mVideoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 3. 获取编码喂养数据的输入源surface
mInputSurface = mVideoEncoder.createInputSurface();
mVideoEncoder.start();
// 4. 创建混合器,但我们不能在这里start,因为我们还没有编码后的视频数据,
// 更没有把编码后的数据以track(轨道)的形式加到合成器。
mMuxer = new MediaMuxer(outputFile.toString(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
public Surface getInputSurface() {
return mInputSurface;
}
... ... ...
}
我们开始跟着注释分析学习:
0、首先从CameraRecordEncoderCore命名上我们知道,这部分是摄像头录制编码的核心工作部分,但并不是工作的流程。大家别先入为主。(并不是这里控制录制视频,这只是录制视频中关键的工具部分)
1、按照官方说明,创建一个编码器我们需要配置编码格式等一系列参数,然后我们通过MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);获取一个AVC(H.264)的编码器,记得是Encoder,不是Decoder。别搞错了。随后我们看看编码器配置Codec.configure的四个参数:第一个MediaFormat,就是想要设置的格式,没啥大问题;第二个Surface,此Surface是在解码器的时候使用的,告诉解码后的视频渲染介质,用于系统直接渲染,提高性能效率;第三个MediaCrypto是媒体加解密,我们这里不用到,先忽略;第四个是指定其Codec是一个编码器。
2、成功创建编码器后,我们从编码器中获取一个InputSurface,这个和刚刚Codec.configure第二个参数OutputSurface相对应。既然Codec充当解码器的时候能指定渲染Surface,那么在Codec充当是编码器的时候,也要有一个输入Surface,在输入Surface渲染的画面就能直接当做输入源流进到编码器中,提高性能和效率。我们开启接口供外部引用这个InputSurface。
3、接着我们利用MediaMuxer创建MP4格式的视频混合器。关于MP4和上面说的AVC(H.264)这些概念如果有搞不清的同学,请(一定要)点击这里前辈大神总结的详细知识。基本概括就是:MP4等一些常见的视频文件,这些文件其实类似一个包裹,它的后缀则是包裹的包装方式。这些包裹里面,包含了视频(只有图像),音频(只有声音),字幕等。当播放器在播放的时候,首先对这个包裹进行拆包(专业术语叫做分离/splitting),把其中的视频、音频等拿出来,再进行播放。既然它们只是一个包裹,就意味着这个后缀不能保证里面的东西是啥,也不能保证到底有多少东西。包裹里面的每一件物品,我们称之为轨道(track)。每个轨道所承载的物件都经过特定的压缩格式(H.264)进行压缩。编码相当于这个压缩这个操作,压缩后的数据我们以轨道(track)为单位打包成MP4的文件,这个操作就是MediaMuxer混合器来完成的。
编码器我们已经准备好了,那么我继续看看应该怎么编码:
private MediaCodec.BufferInfo mBufferInfo;
private int mTrackIndex;
private boolean mMuxerStarted;
private static final int TIMEOUT_USEC = 10000;
/**
* 从编码器中提取所有未处理的数据,并将其转发给Muxer。
* endOfStream是代表是否编码结束的终结符,
* 如果是false就是正常请求输入数据去编码,按正常流程走这次编码操作。
* 如果是true我们需要告诉编码器编码工作结束了,发送一个EOS结束标志位到输入源,
* 然后等到我们在编码输出的数据发现EOS的时候,证明最后的一批编码数据已经编码成功了。
*/
public void drainEncoder(boolean endOfStream) {
if (endOfStream) {
if (DEBUG) Log.d(TAG, "sending EOS to encoder");
mVideoEncoder.signalEndOfInputStream();
}
// 1. 获取编码输出队列
ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
while (true) {
// 2. 从编码的输出队列中检索出各种状态,对应处理。
// 参数一是MediaCodec.BufferInfo,主要是用来承载对应buffer的附加信息。
// 参数二是超时时间,请注意单位是微秒,1毫秒=1000微秒,这里设置10毫秒。
int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if(encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 暂时还没输出的数据能捕获
if (!endOfStream) {
break; // out of while(true){}
} else {
if (DEBUG) Log.d(TAG, "no output available, spinning to await EOS");
}
} else if(encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// 这个状态说明输出队列对象改变了,请重新获取一遍。
encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
} else if(encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 当我们接收到编码后的输出数据,会通过格式已转变这个标志触发,而且只会发生一次格式转变
// 因为不可能从设置指定的格式变成其他,难不成一个视频能有两种编码格式?
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat videoFormat = mVideoEncoder.getOutputFormat();
// 现在我们已经得到想要的编码数据了,让我们开始合成进mp4容器文件里面吧。
mTrackIndex = mMuxer.addTrack(videoFormat);
// 获取track轨道号,等下写入编码数据的时候需要用到
mMuxer.start();
mMuxerStarted = true;
} else if(encoderStatus < 0) {
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
// Continue while(true)
} else {
// 3. 各种状态处理之后,大于0的encoderStatus则是指出了编码数据是在编码队列的具体位置。
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// 这表明,标记为这样的缓冲器包含编解码器初始化/编解码器特定数据而不是媒体数据。
if (DEBUG) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(mBufferInfo.offset);
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
if (DEBUG) {
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
mBufferInfo.presentationTimeUs);
}
}
// 释放 编码器输出队列中 指定位置的buffer,第二个参数指定是否将其buffer渲染到解码Surface
mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.w(TAG, "reached end of stream unexpectedly");
} else {
if (DEBUG) Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
整个方法看着有点复杂,我们慢慢分析:
0、首先这个方法是主动调用的,并附带一个endOfStream的标志符,这个标志符在函数说明的注释已经说明白;如果是false就是正常请求输入数据去编码,按正常流程走这次编码操作。如果是true我们需要告诉编码器编码工作结束了,通过Codec.signalEndOfInputStream发送一个EOS结束标志位到输入源,然后等到我们在编码输出的数据发现EOS的时候,证明最后的一批编码数据已经编码成功了。
1、我们通过mInputSurface渲染(外部调用)画面之后,正常开始编码。先是获取编码输出队列的引用,是一个ByteBuffer的数组,然后我们通过dequeueOutputBuffer请求编码后的buffer出列,返回的是encoderStatus编码状态。根据编码状态我们逐一分析。
2、MediaCodec的编解码标志位有以下三个:
encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER:说明Codec暂时还没输出的数据能捕获,如果不是主动请求EOS结束的,我们可以跳过这次请求编码的申请。
encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:说明 输出队列对象改变了,请重新获取一遍。
encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:当我们接收到编码后的输出数据,会通过格式已转变这个标志触发,而且只会发生一次格式转变,因为不可能从设置指定的格式变成其他,难不成一个视频能有两种编码格式?此时我们就可以开始MP4合成器的工作了。
以上的encoderStatus都是定义成负值小于0的,当如果是encoderStatus大于0的,则是代表编码数据是在编码队列的具体位置,数组的索引值。通过数组索引我们获取特定的ByteBuffer,并检查这ByteBuffer的有效性。确定有效之后,我就根据MediaCodec.BufferInfo的信息调整这组编码后的ByteBuffer。最后我们以track为单位,写入MediaMuxer进行合成。
当我们写入MediaMuxer的数据成功后,我们不急着跳出while(true),因为Codec是异步操作的,我们只管喂养数据,和请求捕获结果。根据官方介绍,我们在捕获消耗数据后,应该将其是否回收到编码器中。通过Codec.releaseOutputBuffer(encoderStatus, false);释放编码器的输出队列中指定位置的buffer,第二个参数指定是否渲染其buffer到解码Surface,这个是Codec为解码器的时候才起作用。我们这里填写为false;
3、最后我们怎么结束编码 和 合成器呢?首先肯定是调用drainEncoder(true); 然后就是release回收资源了。代码如下:
public void release() {
if (VERBOSE) Log.d(TAG, "releasing encoder objects");
if (mVideoEncoder != null ) {
mVideoEncoder.stop();
mVideoEncoder.release();
mVideoEncoder = null;
}
if (mMuxer != null && mTrackIndex != -1) {
// stop() throws an exception if you haven't fed it any data.
// Keep track of frames submitted, and don't call stop() if we haven't written anything.
// Once the muxer stops, it can not be restarted.
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
}
所需的工具我们已经准备好了,下一步我们就要搞清怎么控制录制这个操作,和录制前我们需要什么。说回Codec中的InputSurface,既然有个输入Surface能直接把渲染画面流进Codec,我们为何不把这个Surface结合我们自己的EGL组成EGLSurface,然后按照之前预览帧那样渲染? 还有一点需要注意,录制的EGL环境 和 实时预览的EGL环境渲染是两个独立的工作环境,所以我们的录制是另外一个线程的工作的。 跟随这些思路,我们开始编写CameraRecordEncoder,大致的框架如下:
public class CameraRecordEncoder implements Runnable {
private static final String TAG = "CameraRecordEncoder";
/**
* 编码器设置的bean,为啥不通过构造函数传递。
* 因为通常情况下,构造的时候都还没清楚设置,和还没获取到EGLContext~2333
*/
public static class EncoderConfig {
final File mOutputFile;
final int mWidth;
final int mHeight;
final int mBitRate;
final EGLContext mEglContext;
public EncoderConfig(File outputFile, int width, int height, int bitRate,
EGLContext sharedEglContext) {
mOutputFile = outputFile;
mWidth = width;
mHeight = height;
mBitRate = bitRate;
mEglContext = sharedEglContext;
}
@Override
public String toString() {
return "EncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate +
" to '" + mOutputFile.toString() + "' ctxt=" + mEglContext;
}
}
// ----- 外部线程通信访问 -----
private volatile EncoderHandler mHandler;
private final Object mSyncLock = new Object();
private boolean mReady;
private boolean mRunning;
/**
* 利用handler机制处理外部线程请求编码器的操作。
* 嫌弃自己搭建Thread+Handler麻烦的同学可以用 HandlerThread
*/
class EncoderHandler extends Handler {
private WeakReference mWeakEncoder;
public EncoderHandler(CameraRecordEncoder encoder) {
mWeakEncoder = new WeakReference(encoder);
}
@Override
public void handleMessage(Message msg) {
CameraRecordEncoder encoder = mWeakEncoder.get();
if (encoder == null) {
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
return;
}
}
}
/**
* 开始视频录制。(一般是从其他非录制现场调用的)
* 我们创建一个新线程,并且根据传入的录制配置EncoderConfig创建编码器。
* 我们挂起线程等待正式启动后才返回。
*/
public void startRecording(EncoderConfig encoderConfig) {
Log.d(TAG, "CameraRecordEncoder: startRecording()");
synchronized (mSyncLock) {
if (mRunning) {
Log.w(TAG, "Encoder thread already running");
return;
}
mRunning = true;
new Thread(this, "CameraRecordEncoder").start();
while (!mReady) {
try {
// 等待编码器线程的启动
mSyncLock.wait();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
//mHandler.sendMessage(
// mHandler.obtainMessage(EncoderHandler.MSG_START_RECORDING, encoderConfig) );
}
@Override
public void run() {
Looper.prepare();
synchronized (mSyncLock) {
mHandler = new EncoderHandler(this);
mReady = true;
mSyncLock.notify();
}
Looper.loop();
Log.d(TAG, "Encoder thread exiting");
synchronized (mSyncLock) {
mReady = mRunning = false;
mHandler = null;
}
}
}
注释都很清楚了,反正就是一个独立的工作线程+Handler机制,供外部访问请求编码器的控制。看不懂的先去补补Android的知识吧。 可以知道,CameraRecordEncoder的一切开始都是在startRecording这个方法。但是,我们现在暂且不去理会EncoderHandler.MSG_START_RECORDING的具体实现,反过来思考,我们在原有测试页面ContinuousRecordActivity的预览摄像头的代码上,要怎样处理录像这个操作。只有得到明确的需求,我们才能更好的去实现CameraRecordEncoder。 要不我们就模仿微信的长按录制?一个触碰的按钮,按下状态是请求开始录像(startRecording),手指抬起请求终结录像的录制(stopRecording)。还有在实时预览每一帧的同时(frameAvailable),渲染到我们的CameraRecordEncoder。
SO,这样分析,我们就至少需要三个供外部调用的接口了。startRecording / stopRecording / frameAvailable,现在我们就来编写其余两个,供外部线程访问录像渲染线程操作的方法。
public class CameraRecordEncoder implements Runnable {
... ... ...
public static class EncoderConfig { ... ... //follow github }
// ---------------以下代码 供外部线程通信访问 ---------------------------------------------------------------------------
private volatile EncoderHandler mHandler;
private final Object mSyncLock = new Object();
private boolean mReady;
private boolean mRunning;
/**
* 开始视频录制。(一般是从其他非录制线程调用的)
* 我们创建一个新线程,并且根据传入的录制配置EncoderConfig创建编码器。
* 我们挂起线程等待正式启动后才返回。
*/
public void startRecording(EncoderConfig encoderConfig) {
Log.d(TAG, "CameraRecordEncoder: startRecording()");
synchronized (mSyncLock) {
if (mRunning) {
Log.w(TAG, "Encoder thread already running");
return;
}
mRunning = true;
new Thread(this, "CameraRecordEncoder").start();
while (!mReady) {
try {
// 等待编码器线程的启动
mSyncLock.wait();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_START_RECORDING, encoderConfig));
}
@Override
public void run() {
Looper.prepare();
synchronized (mSyncLock) {
mHandler = new EncoderHandler(this);
mReady = true;
mSyncLock.notify();
}
Looper.loop();
Log.d(TAG, "Encoder thread exiting");
synchronized (mSyncLock) {
mReady = mRunning = false;
mHandler = null;
}
}
/**
* 告诉录像渲染线程停止录像 (一般是从其他非录制线程调用的)
*/
public void stopRecording() {
mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_STOP_RECORDING));
mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_QUIT));
// Codec和Muxer感觉不是立刻结束的,我们是不是应该弄个回调?
}
public void frameAvailable(... ...) {
synchronized (mSyncLock) {
if (!mReady) {
return;
}
}
mHandler.sendMessage(mHandler.obtainMessage(EncoderHandler.MSG_FRAME_AVAILABLE));
}
// 利用handler机制处理外部线程请求编码器的操作。
class EncoderHandler extends Handler {
static final int MSG_START_RECORDING = 0;
static final int MSG_STOP_RECORDING = 1;
static final int MSG_QUIT = 2;
static final int MSG_FRAME_AVAILABLE = 3;
private WeakReference mWeakEncoder;
public EncoderHandler(CameraRecordEncoder encoder) {
mWeakEncoder = new WeakReference(encoder);
}
@Override
public void handleMessage(Message msg) {
CameraRecordEncoder encoder = mWeakEncoder.get();
if (encoder == null) {
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
return;
}
int what = msg.what;
Object obj = msg.obj;
switch (what) {
case MSG_START_RECORDING:
encoder.handleStartRecording((CameraRecordEncoder.EncoderConfig) obj);
break;
case MSG_STOP_RECORDING:
encoder.handleStopRecording(... ...);
break;
case MSG_QUIT:
Looper.myLooper().quit();
// 不能直接在stopRecording中quit,因为调用stopRecording的looper不是我们想退出的线程looper。
break;
}
}
}
// ---------------以上代码 供外部线程通信访问 -------------------------------------------------------------------------
... ... ...
... ... ...
}
CameraRecordEncoder的代码量比较多,希望同学能分清楚其设计思路。这节已经show过两次的设计逻辑,下节我们着重处理外部请求的编码器的操作方法。
The End .
by the way. 祝各位大小朋友儿童节快乐!