最新消息: USBMI致力于为网友们分享Windows、安卓、IOS等主流手机系统相关的资讯以及评测、同时提供相关教程、应用、软件下载等服务。

OpenGL.ES在Android上的简单实践:21

IT圈 admin 2浏览 0评论

OpenGL.ES在Android上的简单实践:21

OpenGL.ES在Android上的简单实践:21-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 上)

1、录制视频需要什么?

在上篇文章,我们已经成功的满足了需求,在预览摄像头的同时加上一些简单的视频二次处理(水印)。接下来我们就是要把视频录制下来,这就涉及视频的编码范畴了。视频编解码知识点无论在哪个平台上的操作系统上,都是比较难的一个知识点。在Android 4.1以前,Android并没有提供硬编硬解的API,所以之前基本上都是采用FFMpeg来做视频软件编解码的,现在FFMpeg在Android的编解码上依旧广泛应用。通常来说,对于同一平台同一硬件环境,硬编硬解的速度是快于软件编解码的。而且相比软件编解码的高CPU占用率来说,硬件编解码也有很大的优势,所以在硬件支持的情况下,一般硬件编解码是我们的首选。 本篇博客主要是利用Android4.1增加的API MediaCodec和Android 4.3增加的API MediaMuxer进行Mp4视频的录制。

既然需要使用系统API进行硬编码录制视频,我们就从官方文档入手(上方连接,需要梯子)看看MediaCodec是怎么玩的。

官网上的图能够很好的说明MediaCodec的使用方式。我们从这两段英文入手理解MediaCodec:

MediaCodec类可用于访问低层媒体编解码器,即编码器/解码器组件。它是Android低层多媒体支持基础设施的一部分。(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.一起联合使用)

广义地说,编解码器处理输入数据以产生输出数据。它异步处理数据,并使用一组输入和输出缓冲区。在一个简单化的层次上,你请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。编解码器使用的数据,并将其转换为其空输出缓冲区之一。最后,请求(或接收)填充的输出缓冲区,消耗其内容并将其释放回编解码器。

好了基本意思就是这样了,我们可以看到,工作原理比较简单,其中有几个关键字:异步(线程工作),编解码(不单指是流转文件,也可以文件转流,或者更实际的网络拉流直播显示),媒体数据(不单只是视频还可以音频)。

2、Let‘s Prepare Record.

废话不多,但我还是想要废话几句的,就是先看看官方的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;               // 30fpsprivate static final int I_FRAME_INTERVAL = 5;          // I-frames 间隔 5sprivate 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 CodingMediaFormat 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. 获取编码喂养数据的输入源surfacemInputSurface = 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渲染到解码SurfacemVideoEncoder.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;}}

3、Let's Start Recording

所需的工具我们已经准备好了,下一步我们就要搞清怎么控制录制这个操作,和录制前我们需要什么。说回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;}@Overridepublic 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<CameraRecordEncoder> mWeakEncoder;public EncoderHandler(CameraRecordEncoder encoder) {mWeakEncoder = new WeakReference<CameraRecordEncoder>(encoder);}@Overridepublic 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) );}@Overridepublic 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));}@Overridepublic 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<CameraRecordEncoder> mWeakEncoder;public EncoderHandler(CameraRecordEncoder encoder) {mWeakEncoder = new WeakReference<CameraRecordEncoder>(encoder);}@Overridepublic 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. 祝各位大小朋友儿童节快乐!

 

OpenGL.ES在Android上的简单实践:21

OpenGL.ES在Android上的简单实践:21-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 上)

1、录制视频需要什么?

在上篇文章,我们已经成功的满足了需求,在预览摄像头的同时加上一些简单的视频二次处理(水印)。接下来我们就是要把视频录制下来,这就涉及视频的编码范畴了。视频编解码知识点无论在哪个平台上的操作系统上,都是比较难的一个知识点。在Android 4.1以前,Android并没有提供硬编硬解的API,所以之前基本上都是采用FFMpeg来做视频软件编解码的,现在FFMpeg在Android的编解码上依旧广泛应用。通常来说,对于同一平台同一硬件环境,硬编硬解的速度是快于软件编解码的。而且相比软件编解码的高CPU占用率来说,硬件编解码也有很大的优势,所以在硬件支持的情况下,一般硬件编解码是我们的首选。 本篇博客主要是利用Android4.1增加的API MediaCodec和Android 4.3增加的API MediaMuxer进行Mp4视频的录制。

既然需要使用系统API进行硬编码录制视频,我们就从官方文档入手(上方连接,需要梯子)看看MediaCodec是怎么玩的。

官网上的图能够很好的说明MediaCodec的使用方式。我们从这两段英文入手理解MediaCodec:

MediaCodec类可用于访问低层媒体编解码器,即编码器/解码器组件。它是Android低层多媒体支持基础设施的一部分。(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.一起联合使用)

广义地说,编解码器处理输入数据以产生输出数据。它异步处理数据,并使用一组输入和输出缓冲区。在一个简单化的层次上,你请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。编解码器使用的数据,并将其转换为其空输出缓冲区之一。最后,请求(或接收)填充的输出缓冲区,消耗其内容并将其释放回编解码器。

好了基本意思就是这样了,我们可以看到,工作原理比较简单,其中有几个关键字:异步(线程工作),编解码(不单指是流转文件,也可以文件转流,或者更实际的网络拉流直播显示),媒体数据(不单只是视频还可以音频)。

2、Let‘s Prepare Record.

废话不多,但我还是想要废话几句的,就是先看看官方的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;               // 30fpsprivate static final int I_FRAME_INTERVAL = 5;          // I-frames 间隔 5sprivate 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 CodingMediaFormat 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. 获取编码喂养数据的输入源surfacemInputSurface = 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渲染到解码SurfacemVideoEncoder.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;}}

3、Let's Start Recording

所需的工具我们已经准备好了,下一步我们就要搞清怎么控制录制这个操作,和录制前我们需要什么。说回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;}@Overridepublic 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<CameraRecordEncoder> mWeakEncoder;public EncoderHandler(CameraRecordEncoder encoder) {mWeakEncoder = new WeakReference<CameraRecordEncoder>(encoder);}@Overridepublic 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) );}@Overridepublic 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));}@Overridepublic 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<CameraRecordEncoder> mWeakEncoder;public EncoderHandler(CameraRecordEncoder encoder) {mWeakEncoder = new WeakReference<CameraRecordEncoder>(encoder);}@Overridepublic 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. 祝各位大小朋友儿童节快乐!

 

发布评论

评论列表 (0)

  1. 暂无评论