博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android视频编解码之MediaCodec简单入门
阅读量:4256 次
发布时间:2019-05-26

本文共 37490 字,大约阅读时间需要 124 分钟。

本篇只是简单入门,后面会继续写文章详细讲解:

由于MediaCodec涉及内容众多,原本想一篇文章把所有内容概括,但是后来发现不太可能,限于自己能力,想要考虑全面太难,我也是刚开始学习需要借助网上的代码进行一步步学习,音视频方面的很多知识也不完善,所以会把MediaCodec分成多个部分进行介绍,谢谢。

Developer:

编解码到底处理什么,前面通过 音视频编解码基础已经回答了这个问题,编码相当于压缩数据。 中onPreviewFrame可以获取Camera中的原始数据,类似NV21或者YV12等类型数据,编码就是可以把这些原始YUV格式数据编码为.h264或者.h265等类型数据,然后利用MediaMuxer把音频和视频数据最终合并成mp4文件等视频类型。解码就是把.h264的数据解码为YUV等原始格式数据。后面还会提到利用Surface作为原始数据源。

"PS:”把原始数据生成h264文件可以利用VLCMediaPlayer进行播放。

1 MediaCodec简介

硬编码和软编码:

硬编码:

用设备GPU去实现编解码,这样可以减轻CPU的压力。
软编码:
让CPU来进行编解码,在c层代码来进行编解码,因为c/c++有很多好的编解码库。

软硬编码对比:

硬编的好处主要在于速度快,而且系统自带不需要引入外部的库,但是特性支持有限,而且硬编的压缩率一般偏低,而对于软编码来说,虽然速度较慢,但是压缩率比较高,而且支持的H264特性也会比硬编码多很多,相对来说比较可控。硬编码会受硬件设备支持的影响。

在Android 4.1之前没有提供硬编解码的API,所以基本都是采用开源的那些库,比如著名的FFMpeg实现软编解码。但是通常情况下,同一平台同一硬件环境,硬编码的速度快于软件编码,软编码使用CPU来进行计算,会消耗一些app的运算效率。在Android4.1出来了一个新的API:MediaCodec可以支持硬编解码,MediaCodec可以支持对音频和视频的编解码.

MediaCodec从api16开始提供,它能够访问更底层的多媒体编解码器组件。MediaCodec是更底层的apis,是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)。

2 MediaCodec的工作模型

在这里插入图片描述

编解码器工作过程为处理输入数据产生输出数据,MediaCodec采用异步方式处理数据,并且使用了一组输入输出缓存(input and output buffers)。

  • 首先请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。
  • 编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。
  • 请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。

3 数据类型(Data Types)

编解码器可以处理三种类型的数据:

  • 压缩数据(即为经过H254. H265. 等编码的视频数据或AAC等编码的音频数据)、
  • 原始音频数据、
  • 原始视频数据。
    三种类型的数据均可以利用ByteBuffers进行处理,但是对于原始视频数据应提供一个Surface以提高编解码器的性能。Surface直接使用本地视频数据缓存(native video buffers),而没有映射或复制数据到ByteBuffers,因此,这种方式会更加高效。也就是说可以如果需要输入的是原始数据可以利用Surface作为源,这样效率更高。

压缩缓存(Compressed Buffers)

上面说了编解码器可以处理三种类型的数据,第一种就是压缩数据,压缩数据可以作为解码器的输入数据或者编码器的输出数据。压缩数据需要指定数据格式,这样编码/解码器才能知道如何处理这些压缩数据。

对于视频一般是包含完整的一帧数据,也就是要告诉解码器从哪到哪是一帧完整的数据或者从编码器得到一帧完整的数据。常用的就是让MediaCodec解码H264数据,必须将分割符和NALU单元作为一个完整的数据帧传给解码器才能正确解码。
对于音频数据通常是单个可访问单元(一个编码的音频片段,通常包含几毫秒的遵循特定格式类型的音频数据),但这种要求也不是十分严格,一个缓存内可能包含多个可访问的音频单元。在这两种情况下,缓存不会在任意的字节边界上开始或结束,而是在帧或可访问单元的边界上开始或结束。

一般都不要传递给MediaCodec不是完整帧的数据,除非是标记了BUFFER_FLAG_PARTIAL_FRAME的数据。`BUFFER_FLAG_PARTIAL_FRAME指示了缓冲区只包含帧的一部分,并且解码器应该对数据进行批处理,直到没有该标志的缓冲区在解码帧之前出现。但是这个标记是API26之后引入的,一般用不到。

原始音频缓存数据(Raw Audio Buffers)

原始的音频数据缓存包含完整的PCM(脉冲编码调制)音频数据帧,这是每一个通道按照通道顺序的一个样本。每一个样本是一个按照本机字节顺序的16位带符号整数(16-bit signed integer in native byte order)。

一段示例代码来获取采样数据:

short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId); MediaFormat format = codec.getOutputFormat(bufferId); ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer(); int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT); if (channelIx < 0 || channelIx >= numChannels) {
return null; } short[] res = new short[samples.remaining() / numChannels]; for (int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx); } return res; }

原始视频缓存(Raw Video Buffers)

在ByteBuffer模式下,视频缓存(video buffers)根据它们的颜色格式(color format)进行展现。你可以通过调用getCodecInfo().getCapabilitiesForType(…).colorFormats方法获得编解码器支持的颜色格式数组。视频编解码器可以支持三种类型的颜色格式:

本地原始视频格式(native raw video format):这种格式通过COLOR_FormatSurface标记,并可以与输入或输出Surface一起使用。

灵活的YUV缓存(flexible YUV buffers)(例如:COLOR_FormatYUV420Flexible):利用一个输入或输出Surface,或在在ByteBuffer模式下,可以通过调用getInput/OutputImage(int)方法使用这些格式。
其他,特定的格式(other, specific formats):通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。这些颜色格式同 flexible format相似,你仍然可以使用 getInput/OutputImage(int)方法。
  从Android 5.1(LOLLIPOP_MR1)开始,所有的视频编解码器都支持灵活的YUV4:2:0缓存(flexible YUV 4:2:0 buffers)。

4 MediaCodec的状态转换

在这里插入图片描述

在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。

利用代码创建了一个编解码器,此时编解码器处于未初始化状态(Uninitialized)。首先,需要使用configure(…)方法对编解码器进行配置,这将使编解码器转为配置状态(Configured)。然后调用start()方法使其转入执行状态(Executing)。在这种状态下可以通过上述的缓存队列操作处理数据。

执行状态(Executing)包含三个子状态: 刷新(Flushed)、运行( Running) 以及流结束(End-of-Stream)。在调用start()方法后编解码器立即进入刷新子状态(Flushed),此时编解码器会拥有所有的缓存。一旦第一个输入缓存(input buffer)被移出队列,编解码器就转入运行子状态(Running),编解码器的大部分生命周期会在此状态下度过。当你将一个带有end-of-stream 标记的输入缓存入队列时,编解码器将转入流结束子状态(End-of-Stream)。在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存(output buffers)直到end-of- stream标记到达输出端。你可以在执行状态(Executing)下的任何时候通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed)。
通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置 。当你使用完编解码器后,你必须调用release()方法释放其资源。
在极少情况下编解码器会遇到错误并进入错误状态(Error)。这个错误可能是在队列操作时返回一个错误的值或者有时候产生了一个异常导致的。通过调用 reset()方法使编解码器再次可用。你可以在任何状态调用reset()方法使编解码器返回到未初始化状态(Uninitialized)。否则,调用 release()方法进入最终的Released状态。

5 创建编解码器

两种方式创建编解码器:一种利用媒体格式name,一种利用媒体格式类型。

可以根据指定的MediaFormat使用MediaCodecList创建一个MediaCodec实例。在解码文件或数据流时,你可以通过调用MediaExtractor.getTrackFormat方法获得所期望的格式(media format),并调用MediaFormat.setFeatureEnabled方法注入任何你想要添加的特定属性,然后调用MediaCodecList.findDecoderForFormat方法获得可以处理指定的媒体格式的编解码器的名字,最后,通过调用createByCodecName(String)方法创建一个编解码器。

注意:在Android 5.0 (LOLLIPOP)上,传递给MediaCodecList.findDecoder/EncoderForFormat的格式不能包含帧率-frame rate。通过调用format.setString(MediaFormat.KEY_FRAME_RATE, null)方法清除任何存在于当前格式中的帧率。

也可以根据MIME类型利用createDecoder/EncoderByType(String)方法创建一个你期望的编解码器。然而,这种方式不能够给编解码器加入指定特性,而且创建的编解码器有可能不能处理你所期望的媒体格式。

创建安全的解码器(Creating secure decoders),所谓安全就是避免创建系统不支持的编解码器,所以Google推荐利用createByCodecName创建编解码器,但是一般我们为了简单可以利用type创建。

在Android 4.4(KITKAT_WATCH)及之前版本,安全的编解码器(secure codecs)没有被列在MediaCodecList中,但是仍然可以在系统中使用。安全编解码器只能够通过名字进行实例化,其名字是在常规编解码器的名字后附加.secure标识(所有安全编解码器的名字都必须以.secure结尾),调用createByCodecName(String)方法创建安全编解码器时,如果系统中不存在指定名字的编解码器就会抛出IOException异常。

从Android 5.0(LOLLIPOP)及之后版本,你可以在媒体格式中使用FEATURE_SecurePlayback属性来创建一个安全编解码器。

创建编解码器会用到MediaFormat,假如是本地文件或者网络流,可以用MediaExtractor.getTrackFormat这个方法来提取MediaFormat信息。如果是原始数据,就需要我们字节创建音频或者视频的MediaFormat。

创建代码示例:

上面提到创建方式有两种:

(1)根据名字创建:

MediaCodec createByCodecName (String name);

//步骤:MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);mediaCodecList.findDecoderForFormat();mediaCodecList.findEncoderForFormat();//返回值为decoder 或者 encoder的名字MediaCodec.createByCodecName();MediaCodecList.REGULAR_CODECS:表示api21之前的MediaCodecList.ALL_CODECS 表示所有的

从类型获取name

private static MediaCodecInfo selectCodec(String mimeType) {
// 获取所有支持编解码器数量 int numCodecs = MediaCodecList.getCodecCount(); for (int i = 0; i < numCodecs; i++) {
// 编解码器相关性信息存储在MediaCodecInfo中 MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); // 判断是否为编码器 if (!codecInfo.isEncoder()) {
continue; } // 获取编码器支持的MIME类型,并进行匹配 String[] types = codecInfo.getSupportedTypes(); for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo; } } } return null; }

(2)根据类型:

MediaCodec createXXXByType (String type);

MediaCodec.createDecoderByType();
MediaCodec.createEncoderByType()

MediaCodec支持的视频格式有vp8 、VP9 、H.264、H.265、MPEG4、H.263等。

MediaCodec支持的音频格式有3gpp、amr-wb、amr-wb、amr-wb、g711-A、g711-U 、AAC等。
类型:
“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/amr-wb” - MPEG1/2 audio layer III
“audio/mp4a-latm” - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
“audio/amr-wb” - vorbis audio
“audio/g711-alaw” - G.711 alaw audio
“audio/g711-mlaw” - G.711 ulaw audio
。。。。。。。

利用媒体类型创建编码器:

public AvcEncoder(int width, int height, int framerate, int bitrate) {
m_width = width; m_height = height; m_framerate = framerate; MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);//传入类型,和宽高 mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);//设置颜色格式对应NV21 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);//帧传输速率 mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);//设置fps,一般20 或者30 mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//关键帧的延时 //mediaFormat.setInteger(MediaFormat.KEY_ROTATION,90); try {
mediaCodec = MediaCodec.createEncoderByType("video/avc"); } catch (IOException e) {
e.printStackTrace(); } mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mediaCodec.start(); }

6 初始化

创建好编解码器后需要设置一系列的参数来初始化它,编解码器提供了处理数据的两种方式:同步和异步。假如是异步的方式的话,必须要在configure之前来为它设置回调函数,设置好回调函数后,就可以调用configure函数了。如果不利用回调函数的方式,则不需要设置。然后,使用指定的媒体格式配置编解码器。这时你可以为视频原始数据产生者(例如视频解码器)指定输出Surface。此时你也可以为secure 编解码器设置解密参数(详见MediaCrypto) 。最后,因为编解码器可以工作于多种模式,你必须指定是该编码器是作为一个解码器(decoder)还是编码器(encoder)运行。

看configure函数,上面提到了解密参数就对应第一个函数,这里不再展开讲。
void configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
void configure (MediaFormat format, Surface surface, int flags, MediaDescrambler descrambler)
在不解码生成outputbuffers的时候或者不想将生成的outputbuffers渲染到surface的时候,可以设置为null。flag有两种模式,CONFIGURE_FLAG_ENCODE(值等于1)为编码模式,其他的值为解码模式。

从API LOLLIPOP起,你可以在Configured 状态下查询输入和输出格式的结果。在开始编解码前你可以通过这个结果来验证配置的结果,例如,颜色格式。

假如想处理原始视频帧,需要将原始视频帧编码为类似于h264或者其他格式,需要调用createInputSurface()方法产生一个surface,并且必须在configure之后,这个surface上目前是空数据,然后调用start方法,当有数据注入到surface时,mediacodec就能立即获取到并解码。假如是api23之后,也可以使用Surface createPersistentInputSurface ()来创建一个surface,其他的编码器可以调用setInputSurface(Surface)方法来继续使用这个surface。

7 配置编解码器:

编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native-configure实现对编解码器的配置工作。在配置时,configure方法需要传入format、surface、crypto、flags参数,其中format为MediaFormat的实例,它使用”key-value”键值对的形式存储多媒体数据格式信息;surface用于指明解码器的数据源来自于该surface;crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。

// 创建MediaFormatMediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480);     mFormat.setInteger(MediaFormat.KEY_BIT_RATE,600);       // 指定比特率mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);  // 指定帧率mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat); // 指定编码器颜色格式 ,这里的颜色格式在前面已经介绍 mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,10); // 指定关键帧时间间隔mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); //最后一个参数指定是编码器还是解码器。

以上代码是在编码H.264时的配置方法,createVideoFormat(“video/avc”, 640 ,480)为”video/avc”类型(即H.264)编码器的MediaFormat对象,需要指定视频数据的宽高,如果编解码音频数据,则调用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法。

当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。

如何指定颜色格式:

在ByteBuffer模式下,视频缓存(video buffers)根据它们的颜色格式(color format)进行展现。可以通过调用getCodecInfo().getCapabilitiesForType(…).colorFormats方法获得编解码器支持的颜色格式数组。
利用MediaFormat.KEY_COLOR_FORMAT配置颜色格式属性,该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。比如,Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式。

视频编解码支持三种色彩格式:

  • native raw video format : COLOR_FormatSurface,可以用来处理surface模式的数据输入输出。
  • flexible YUV buffers : 例如COLOR_FormatYUV420Flexible,可以用来处理surface模式的输出输出,在使用ByteBuffer模式的时候可以用getInput/OutputImage(int)方法来获取image数据,都是以COLOR_FormatYUV420开头的。
  • specific formats: 支持ByteBuffer模式,有一些厂家会定制, 其他的在MediaCodecInfo.CodecCapabilities中可以看到,格式较多, 假如是flexible format, 同样可以使用Image来处理数据,getInput/OutputImage(int)。
    常见颜色格式映射如下:
    NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar
    NV21 ———-> COLOR_FormatYUV420SemiPlanar
    YV12(I420) ———-> COLOR_FormatYUV420Planar

YUV420的颜色格式分类:

YUV420 数据在内存中的长度是 width * hight * 3 / 2
YUV420P:YV12、I420
YUV420SP:NV12、NV21
YUV420P 和 YUV420的区别 :
它们在存储格式上有区别?
YUV420P:yyyyyyyy uuuuuuuu vvvvv yuv420: yuv yuv yuv
YUV420P,Y,U,V三个分量都是平面格式,分为I420和YV12。I420格式和YV12格式的不同处在U平面和V平面的位置不同。在I420格式中,U平面紧跟在Y平面之后,然后才是V平面(即:YUV);但YV12则是相反(即:YVU)。
YUV420SP, Y分量平面格式,UV打包格式, 即NV12。 NV12与NV21类似,U 和 V 交错排列,不同在于UV顺序。
I420: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>YUV420P
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>YUV420SP
 从Android 5.1(LOLLIPOP_MR1)开始,所有的视频编解码器都支持灵活的YUV4:2:0缓存(flexible YUV 4:2:0 buffers)。
 获取video/avc 对应的编码器的颜色格式

public void getMediaCodecList(){
//获取所有的解码器 int codecsNums = MediaCodecList.getCodecCount(); MediaCodecInfo codecInfo = null; for(int i = 0; i < codecsNums && codecInfo == null ; i++){
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); if(info.isEncoder()){
System.out.println("========这是一个编码器=========="); }else{
System.out.println("========这是一个解码器=========="); continue; } String[] types = info.getSupportedTypes(); boolean found = false; for(int j=0; j

result:

=capabilities.colorFormats.length=4
formatformat
2135033992
formatformat
==19
=COLOR_FormatYUV420Planar
formatformat
21
=COLOR_FormatYUV420SemiPlanar
formatformat
2130708361

关于颜色格式这部分其实没那么重要,只需要记住:

NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar
NV21 ———-> COLOR_FormatYUV420SemiPlanar
YV12(I420) ———-> COLOR_FormatYUV420Planar

8 指定特殊数据

有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。当处理这种压缩格式的数据时,这些数据必须在调用start()方法后且在处理任何帧数据之前提交给编解码器。这些数据必须在调用queueInputBuffer方法时使用BUFFER_FLAG_CODEC_CONFIG进行标记。

  前缀信息通常包含在数据中,但是需要自己提取出来,在mediacodec执行start之后提交这些数据,比如h264的sps和pps,在queueinputbuffer的时候flag设置为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提交。
mediaFormat.setByteBuffer(“csd-0”, ByteBuffer.wrap(sps));//sps是一个包含sps信息的byte数组
mediaFormat.setByteBuffer(“csd-1”, ByteBuffer.wrap(pps));//pps是一个包含pps信息的byte数组
  
  Android 使用下列的codec-specific data buffers。对于适当的MediaMuxer轨道配置,这些也要在轨道格式中进行设置。每一个参数集以及被标记为(*)的codec-specific-data段必须以”\x00\x00\x00\x01"字符开头。
在这里插入图片描述
 注意:当编解码器被立即刷新或start之后不久刷新,并且在任何输出buffer或输出格式变化被返回前需要特别地小心,因为编解码器的codec specific data可能会在flush过程中丢失。为保证编解码器的正常运行,你必须在刷新后使用标记为BUFFER_FLAG_CODEC_CONFIG 的buffers再次提交这些数据。

编码器(或者产生压缩数据的编解码器)将会在有效的输出缓存之前产生和返回编解码器指定的数据,这些数据会以codec-config flag进行标记。包含codec-specific-data的Buffers没有有意义的时间戳。

   许多解码器要求实际压缩数据流以“codec specific data”为先导,也就是用于初始化codec的设置数据,例如AVC视频情况时的PPS/SPS,或vorbis音频情况时的code tables。MediaExtractor类提供codec specific data作为返回的track format的一部分,在命名为csd-0,csd-1的条目中。

通过指定BUFFER_FLAG_CODEC_CONFIG,这些buffers可以在start()或flush()后直接提交。然而,如果你使用包含这些keys的MediaFormat配置codec,他们将在start后自动地提交。因此,不鼓励使用BUFFER_FLAG_CODEC_CONFIG,仅推荐高级用户使用。

一般不需要自己生成设置“codec specific data”,只需要记录H264得编码首帧,内部存有SPS和PPS信息,需要保留起来,然后,加在每个H264关键帧的前面。

Thread EncoderThread = new Thread(new Runnable() {
@SuppressLint("NewApi") @Override public void run() {
isRuning = true; byte[] input = null; long pts = 0; long generateIndex = 0; while (isRuning) {
if (YUVQueue.size() >0){
input = YUVQueue.poll(); byte [] tempinput = rotateYUV420Degree90(input, m_height, m_width); byte[] yuv420sp = new byte[m_width*m_height*3/2]; NV21ToNV12(tempinput,yuv420sp,m_width,m_height); input = yuv420sp; } if (input != null) {
try {
long startMs = System.currentTimeMillis(); ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) {
pts = computePresentationTime(generateIndex); ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(input); mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); generateIndex += 1; } /*H264编码首帧,内部存有SPS和PPS信息,需要保留起来,然后,加在每个H264关键帧的前面。 * 其中有个字段是flags,它有几种常量情况。 flags = 4;End of Stream。 flags = 2;首帧信息帧。 flags = 1;关键帧。 flags = 0;普通帧。*/ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_TIME); while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; byte[] outData = new byte[bufferInfo.size]; outputBuffer.get(outData); if(bufferInfo.flags == 2){
//首帧,记录信息 configbyte = new byte[bufferInfo.size]; configbyte = outData; }else if(bufferInfo.flags == 1){
byte[] keyframe = new byte[bufferInfo.size + configbyte.length]; System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length); System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length); outputStream.write(keyframe, 0, keyframe.length); }else{
outputStream.write(outData, 0, outData.length); } mediaCodec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_TIME); } } catch (Throwable t) {
t.printStackTrace(); } } else {
try {
Thread.sleep(500); } catch (InterruptedException e) {
e.printStackTrace(); } } } } }); EncoderThread.start();

9 数据处理(Data Processing)

根据API版本情况,你有三种处理相关数据的方式:

在这里插入图片描述
  每一个编解码器都包含一组输入和输出缓存(input and output buffers),这些缓存在API调用中通过buffer-id进行引用。当成功调用start()方法后客户端将不会“拥有”输入或输出buffers。在同步模式下,通过调用dequeueInput/OutputBuffer(…) 方法从编解码器获得(取得所有权)一个输入或输出buffer。在异步模式下,你可以通过MediaCodec.Callback.onInput/OutputBufferAvailable(…)的回调方法自动地获得可用的buffers。

在获得一个输入buffe后,向其中填充数据,并利用queueInputBuffer方法将其提交给编解码器,若使用解密,则利用queueSecureInputBuffer方法提交。不要提交多个具有相同时间戳的输入buffers(除非它是也被同样标记的codec-specific data)。

在异步模式下通过onOutputBufferAvailable方法的回调或者在同步模式下响应dequeuOutputBuffer的调用,编解码器返回一个只读的output buffer。在这个output buffer被处理后,调用一个releaseOutputBuffer方法将这个buffer返回给编解码器。

当你不需要立即向编解码器重新提交或释放buffers时,保持对输入或输出buffers的所有权可使编解码器停止工作,当然这些行为依赖于设备情况。特别地,编解码器可能延迟产生输出buffers直到输出的buffers被释放或重新提交。因此,尽可能短时间地持有可用的buffers。

使用缓存的异步处理方式(Asynchronous Processing using Buffers)

   从Android 5.0(LOLLIPOP)开始,首选的方法是调用configure之前通过设置回调异步地处理数据。异步模式稍微改变了状态转换方式,因为你必须在调用flush()方法后再调用start()方法才能使编解码器的状态转换为Running子状态并开始接收输入buffers。同样,初始调用start方法将编解码器的状态直接变化为Running 子状态并通过回调方法开始传递可用的输入buufers。
在这里插入图片描述
异步模式下,典型的使用示例如下:

MediaCodec codec = MediaCodec.createByCodecName(name); MediaFormat mOutputFormat; // member variable codec.setCallback(new MediaCodec.Callback() {
@Override void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } @Override void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is equivalent to mOutputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } @Override void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) mOutputFormat = format; // option B } @Override void onError(…) {
… } }); codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();

使用缓存的同步处理方式(Synchronous Processing using Buffers)

从Android5.0(LOLLIPOP)开始,即使在同步模式下使用编解码器你应该通过getInput/OutputBuffer(int) 或 getInput/OutputImage(int) 方法检索输入和输出buffers。这允许通过框架进行某些优化,例如,在处理动态内容过程中。如果你调用getInput/OutputBuffers()方法这种优化方式是不可用的。
注意,不要同时混淆使用缓存和缓存数组的方法。特别地,仅仅在调用start()方法后或取出一个值为INFO_OUTPUT_FORMAT_CHANGED的输出buffer ID后你才可以直接调用getInput/OutputBuffers方法。
同步模式下MediaCodec的典型应用如下:

MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…); // 通过有效数据来装填inputBuffer … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // 缓存格式等同于输出格式 // 输出格式已经准备好了执行和渲染 … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//数据流会遵循新的格式 //如果使用getOutputFormat(outputBufferId),则会被忽略 outputFormat = codec.getOutputFormat(); // option B } } codec.stop(); codec.release();

使用缓存数组的同步处理方式(Synchronous Processing using Buffer Arrays)-- (deprecated)

MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); codec.start(); ByteBuffer[] inputBuffers = codec.getInputBuffers(); ByteBuffer[] outputBuffers = codec.getOutputBuffers(); for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…); if (inputBufferId >= 0) {
// 通过有效数据来装填InputBuffers[inputBufferId] … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) {
// outputBuffers[outputBufferId]已经准备好了被执行或渲染 … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers(); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 数据流会遵循新的格式 MediaFormat format = codec.getOutputFormat(); } } codec.stop(); codec.release();

上线讲述了三种方式进行操作, 前两种方式使用更简单,且内部进行了优化,但是从5.0才开始支持,所以如果要支持4.+的版本还是要用最后一个版本的方式。

注意:

H264编码首帧,内部存有SPS和PPS信息,需要保留起来,然后,加在每个H264关键帧的前面。
mediaCodec.dequeueOutputBuffer会返回MediaCodec.BufferInfo。
其中有个字段是flags,它有几种常量情况。
flags = 4;End of Stream。
flags = 2;首帧信息帧。
flags = 1;关键帧。
flags = 0;普通帧。

flags2 时是首帧信息,需要记录内部的sps和pps信息,保留起来,在获取flags1时添加到头部中。

如果送来的流的第一帧Frame有pps和sps,可以不需要配置format.setByteBuffer的”csd-0” (sps) 和”csd-1”(pps);

否则必须配置相应的pps和sps,通常情况下sps和pps如下:
byte[] sps = { 0, 0, 0, 1, 103, 100, 0, 40, -84, 52, -59, 1, -32, 17, 31, 120, 11, 80, 16, 16, 31, 0, 0, 3, 3, -23, 0, 0, -22, 96, -108 };
byte[] pps = { 0, 0, 0, 1, 104, -18, 60, -128 };。

codec.queueInputBuffer(index,0,0,0,BUFFER_FLAG_END_OF_STREAM);//第三个时间戳可以随意设置

前边提到过,接受到这个信号后,codec将不再接受任何新的数据,在这个信号之前的数据会全部输出。

使用surface作为输出

使用surface做为输出时与使用Bytebuffer基本一致,只是在surface模式下所有的bytebuffer和image全部为null。

释放:

releaseOutputBuffer(bufferId, false); //不会渲染到surface上
releaseOutputBuffer(bufferId, true); //使用默认的时间戳渲染视频
releaseOutputBuffer(bufferId, timestamp) //使用指定的时间戳渲染视频

10 缓存数组的方式代码示例

布局文件:

Activity代码,为了简单,没有提取代码

录制视频利用编码器编码存储到本地

import android.Manifest;import android.annotation.SuppressLint;import android.annotation.TargetApi;import android.app.Activity;import android.content.Context;import android.content.pm.PackageManager;import android.graphics.ImageFormat;import android.hardware.Camera;import android.hardware.Camera.Parameters;import android.hardware.Camera.PreviewCallback;import android.media.MediaCodec;import android.media.MediaCodecInfo;import android.media.MediaCodecList;import android.media.MediaFormat;import android.os.Build;import android.os.Bundle;import android.os.Environment;import android.support.annotation.NonNull;import android.support.v4.app.ActivityCompat;import android.support.v4.content.ContextCompat;import android.util.Log;import android.view.Surface;import android.view.SurfaceHolder;import android.view.SurfaceView;import android.view.View;import android.view.WindowManager;import android.widget.Button;import android.widget.LinearLayout;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.util.concurrent.ArrayBlockingQueue;public class MainActivity1 extends Activity  implements SurfaceHolder.Callback,PreviewCallback{
private static final String TAG = "lidongxiu"; private static int mOrientation = 0; private int TIMEOUT_TIME = 10000; private SurfaceView mSurfaceview; private Button mStopRecord; private SurfaceHolder surfaceHolder; private Camera mCamera; private Parameters parameters; //Camera设置的预览宽高 int width = 640; int height = 480; //帧率20 或者 30 都可以,30已经相对来说效果最好了 int mFramerate = 30; //比特率,可以调节,如果太大可以调小,太大会导致卡顿 int biteRate = width*height*30*8; //最多存储多少帧的数据 public int mQueuesize = 10; public ArrayBlockingQueue
YUVQueue = new ArrayBlockingQueue
(mQueuesize); private MediaCodec mMediaCodec; //存储第一帧的数据,添加到关键帧的前面 public byte[] mFirstFrameConfig; private static String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/mediacodecDemo.h264"; private BufferedOutputStream outputStream; public boolean isRuning = false; private boolean havePermission = false; public int mCameraId; @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main1); // Android 6.0相机动态权限检查,省略了 mSurfaceview = (SurfaceView)findViewById(R.id.surfaceview); mStopRecord = findViewById(R.id.stoprecord); getMediaCodecList(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
havePermission = true; init(); } else {
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE }, 100); } } private void init() {
mStopRecord.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
if (null != mCamera) {
mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; StopThread(); } } }); WindowManager wm = (WindowManager) MainActivity1.this.getSystemService(Context.WINDOW_SERVICE); int width = wm.getDefaultDisplay().getWidth(); LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mSurfaceview.getLayoutParams(); layoutParams.width = width; layoutParams.height = width*4/3; mSurfaceview.setLayoutParams(layoutParams); surfaceHolder = mSurfaceview.getHolder(); surfaceHolder.addCallback(this); isSupportAvcCodec(); } @Override public void surfaceCreated(SurfaceHolder holder) {
mCamera = getBackCamera(); calculateCameraPreviewOrientation(this); startcamera(mCamera); createEncoder(); StartEncoderThread(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
} @Override public void surfaceDestroyed(SurfaceHolder holder) {
if (null != mCamera) {
mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; StopThread(); } } @Override public void onPreviewFrame(byte[] data, Camera camera) {
putYUVData(data,data.length); } public void putYUVData(byte[] buffer, int length) {
if (YUVQueue.size() >= 10) {
YUVQueue.poll(); } YUVQueue.add(buffer); } @SuppressLint("NewApi") private boolean isSupportAvcCodec(){
if(Build.VERSION.SDK_INT>=18){
for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); String[] types = codecInfo.getSupportedTypes(); for (int i = 0; i < types.length; i++) {
if (types[i].equalsIgnoreCase("video/avc")) {
return true; } } } } return false; } private void startcamera(Camera mCamera){
if(mCamera != null){
try {
mCamera.setPreviewCallback(this); mCamera.setDisplayOrientation(mOrientation); if(parameters == null){
parameters = mCamera.getParameters(); } parameters = mCamera.getParameters(); parameters.setPreviewFormat(ImageFormat.NV21); parameters.setPreviewSize(width, height); mCamera.setParameters(parameters); mCamera.setPreviewDisplay(surfaceHolder); mCamera.startPreview(); } catch (IOException e) {
e.printStackTrace(); } } } @TargetApi(9) private Camera getBackCamera() {
Camera c = null; try {
c = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK; } catch (Exception e) {
e.printStackTrace(); } return c; } @SuppressLint("NewApi") //获取video/avc 对应的编码器的颜色格式 public void getMediaCodecList(){
//获取所有的解码器 int codecsNums = MediaCodecList.getCodecCount(); MediaCodecInfo codecInfo = null; for(int i = 0; i < codecsNums && codecInfo == null ; i++){
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); if(info.isEncoder()){
System.out.println("========这是一个编码器=========="); }else{
System.out.println("========这是一个解码器=========="); continue; } String[] types = info.getSupportedTypes(); boolean found = false; for(int j=0; j
0){
input = YUVQueue.poll(); byte [] tempinput = rotateYUV420Degree90(input, width, height); byte[] yuv420sp = new byte[width*height*3/2]; NV21ToNV12(tempinput,yuv420sp,height,width); input = yuv420sp; } if (input != null) {
try {
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers(); int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) {
pts = computePresentationTime(generateIndex); ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(input); mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); generateIndex += 1; } /*H264编码首帧,内部存有SPS和PPS信息,需要保留起来,然后,加在每个H264关键帧的前面。 * 其中有个字段是flags,它有几种常量情况。 flags = 4;End of Stream。 flags = 2;首帧信息帧。 flags = 1;关键帧。 flags = 0;普通帧。*/ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_TIME); while (outputBufferIndex >= 0) {
//Log.i("AvcEncoder", "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+""); ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; byte[] outData = new byte[bufferInfo.size]; outputBuffer.get(outData); if(bufferInfo.flags == 2){
//首帧,记录信息 mFirstFrameConfig = new byte[bufferInfo.size]; mFirstFrameConfig = outData; }else if(bufferInfo.flags == 1){
byte[] keyframe = new byte[bufferInfo.size + mFirstFrameConfig.length]; System.arraycopy(mFirstFrameConfig, 0, keyframe, 0, mFirstFrameConfig.length); System.arraycopy(outData, 0, keyframe, mFirstFrameConfig.length, outData.length); outputStream.write(keyframe, 0, keyframe.length); }else{
outputStream.write(outData, 0, outData.length); } mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_TIME); } } catch (Throwable t) {
t.printStackTrace(); } } else {
try {
Thread.sleep(500); } catch (InterruptedException e) {
e.printStackTrace(); } } } } }); EncoderThread.start(); } private void NV21ToNV12(byte[] nv21,byte[] nv12,int width,int height){
if(nv21 == null || nv12 == null)return; int framesize = width*height; int i = 0,j = 0; System.arraycopy(nv21, 0, nv12, 0, framesize); for(i = 0; i < framesize; i++){
nv12[i] = nv21[i]; } for (j = 0; j < framesize/2; j+=2) {
nv12[framesize + j-1] = nv21[j+framesize]; } for (j = 0; j < framesize/2; j+=2) {
nv12[framesize + j] = nv21[j+framesize-1]; } } /** * Generates the presentation time for frame N, in microseconds. */ private long computePresentationTime(long frameIndex) {
return 132 + frameIndex * 1000000 / mFramerate; } /** * 此处为顺时针旋转旋转90度 * @param data 旋转前的数据 * @param imageWidth 旋转前数据的宽 * @param imageHeight 旋转前数据的高 * @return 旋转后的数据 */ private byte[] rotateYUV420Degree90(byte[] data, int imageWidth, int imageHeight) {
byte [] yuv = new byte[imageWidth*imageHeight*3/2]; // Rotate the Y luma int i = 0; for(int x = 0;x < imageWidth;x++) {
for(int y = imageHeight-1;y >= 0;y--) {
yuv[i] = data[y*imageWidth+x]; i++; } } // Rotate the U and V color components i = imageWidth*imageHeight*3/2-1; for(int x = imageWidth-1;x > 0;x=x-2) {
for(int y = 0;y < imageHeight/2;y++) {
yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+x]; i--; yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+(x-1)]; i--; } } return yuv; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) {
// 相机权限 case 100: havePermission = true; init(); break; } } /** * 设置预览角度,setDisplayOrientation本身只能改变预览的角度 * previewFrameCallback以及拍摄出来的照片是不会发生改变的,拍摄出来的照片角度依旧不正常的 * 拍摄的照片需要自行处理 * 这里Nexus5X的相机简直没法吐槽,后置摄像头倒置了,切换摄像头之后就出现问题了。 * @param activity */ public int calculateCameraPreviewOrientation(Activity activity) {
Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(mCameraId, info); int rotation = activity.getWindowManager().getDefaultDisplay() .getRotation(); int degrees = 0; switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } int result; if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360; result = (360 - result) % 360; } else {
result = (info.orientation - degrees + 360) % 360; } mOrientation = result; System.out.println("=========orienttaion============="+result); return result; }}

在这里插入图片描述

生成的h264资源可以利用VLC MediaPlayer 播放:
在这里插入图片描述
ps:
在用android MediaCodec编码h264的时候,会遇到,dequeueOutputBuffer在成功获取到config帧(sps pps)及第一个I帧后,dequeueOutputBuffer然后结果一直为-1的情况。解决:queueInputBuffer第四个参数时间戳一定要写。

转载地址:http://kxpei.baihongyu.com/

你可能感兴趣的文章
文件下载问题
查看>>
ASP.NET返回上一页的方法小集
查看>>
神经网络中 BP 算法的原理与 Python 实现源码解析
查看>>
腾讯实习生面试总结
查看>>
Jfinal中的render
查看>>
STL中的Allocator
查看>>
STL中的Iterator
查看>>
C语言拾遗
查看>>
数据库查询语句拾遗
查看>>
STL中的Vector
查看>>
C++中的trivial、standard layout、POD
查看>>
阿里中间件三大存储系统
查看>>
Tair源码阅读1---ConfigServer
查看>>
STL中的RB-tree
查看>>
STL中的Sort
查看>>
LeetCode---3.TreeEasy
查看>>
基于比较的排序算法的最优下界---NlogN
查看>>
Paxos协议学习---2.由3大条件证明一致性
查看>>
Paxos协议学习---3.Paxos Made Simple
查看>>
C/C++输入输出
查看>>