Android原生编解码接口 MediaCodec 之——完全解析

2024-05-20 05:50

1. Android原生编解码接口 MediaCodec 之——完全解析

 MediaCodec 是Android 4.1(api 16)版本引入的编解码接口, Developer 官网 上描述的已经很清楚了。可以配合 中文翻译 一起看。理解更深刻。
   MediaCodec的工作流程:
                                           从上图可以看出 MediaCodec 架构上采用了2个缓冲区队列,异步处理数据,并且使用了一组输入输出缓存。   你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。   具体工作如下:
   MediaCodec的基本调用流程是:
   1.初始化MediaCodec,方法有两种,分别是通过名称和类型来创建,对应的方法为:
   2.配置编码器,设置各种编码器参数(MediaFormat),这个类包含了比特率、帧率、关键帧间隔时间等。然后再调用 mMediaCodec .configure,对于 API 19 以上的系统,我们可以选择 Surface 输入:mMediaCodec .createInputSurface,
   3.打开编码器,获取输入输出缓冲区
   获取输入输出缓冲区在api19 上是以上方式获取,api21以后 可以使用直接获取ByteBuffer
   4.输入数据,有2种方式,一种是普通输入,一种是Surface 输入   普通输入又可区分为两种情况,一种是配合MediaExtractor ,一种是取原数据;
   返回一个填充了有效数据的input buffer的索引,如果没有可用的buffer则返回-1,参数为超时时间(TIMES_OUT),单位是微秒,当timeoutUs==0时,该方法立即返回;当timeoutUs0时,   等待时间为传入的微秒值。
   上面输入缓存的index,通过getInputBuffers()得到的是输入缓存数组,通过index和输入缓存数组可以得到当前请求的输入缓存,在使用之前要clear一下,避免之前的缓存数据影响当前数据,接着就是把数据添加到输入缓存中,并调用queueInputBuffer(...)把缓存数据入队;
   5.输出数据   通常编码传输时每个关键帧头部都需要带上编码配置数据(PPS,SPS),但 MediaCodec 会在首次输出时专门输出编码配置数据,后面的关键帧里是不携带这些数据的,所以需要我们手动做一个拼接;
   6.使用完MediaCodec后释放资源   要告知编码器我们要结束编码,Surface 输入的话调用 mMediaCodec .signalEndOfInputStream,普通输入则可以为在 queueInputBuffer 时指定 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag;告知编码器后我们就可以等到编码器输出的 buffer 带着 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag 了,等到之后我们调用 mMediaCodec .release 销毁编码器
   流控就是流量控制。 为什么要控制,就是为了在一定的限制条件下,收益最大化!    涉及到了 TCP 和视频编码:   对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。
   TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。
   视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。   一般编码器都可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,因为在编码过程中实际可以控制的并不是最终输出的码率,而是编码过程中的一个量化参数(Quantization Parameter,QP),它和码率并没有固定的关系,而是取决于图像内容。 这一点不在这里展开,感兴趣的朋友可以阅读视频压缩编码和音频压缩编码的基本原理。
   无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。 这种情况通常的表现是画面出现很多小方块,看上去像是打了马赛克一样,导致画面的局部或者整体看不清楚的情况 
    配置时指定目标码率和码率控制模式: 
   码率控制模式有三种:   码率控制模式在  MediaCodecInfo.EncoderCapabilities 类中定义了三种,在 framework 层有另一套名字和它们的值一一对应:
   动态调整目标码率:
   Android 流控策略选择
   下面展示使用MediaExtractor获取数据后,用MediaMuxer重新写成一个MP4文件的简单栗子

Android原生编解码接口 MediaCodec 之——完全解析

2. Android MediaCodec

  MediaCodec 类为开发者提供了能访问到Android底层媒体 Codec (Encoder/Decoder)的能力,它是Android底层多媒体基础架构的一部分(通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack一起使用)。   
                                           
    Codec 对三种类型类型的数据起作用: 编码后的压缩数据 , 原始视频数据 , 原始音频数据 。这三种类型的数据都可以通过 ByteBuffer 来传递给 Codec ,但是对于 原始视频数据 我们建议使用 Surface 来传递,这样可以提高 Codec 的性能, Surface 使用的是 native video buffer ,不用映射或者拷贝成 ByteBuffer ,因此这样的方式更高效。当你使用 Surface 来传递 原始视频数据 时,也就无法获取到了 原始视频数据 ,Android 提供了 ImageReader 帮助你获取到解码后的 原始视频数据 。这种方式可能仍然有要比 ByteBuffer 的方式更加高效,因为某些 native video buffer 会直接映射成 byteBuffer 。当然如果你 ByteBuffer 的模式,你可以使用 Image 类提供的 getInput/OutputImage(int) 来获取 原始视频数据 。
   给 Decoder 输入的 InputBuffer 或者 Encoder 输出的 outputBuffer 包含的都是编码后的压缩数据,数据的压缩类型由 MediaFormat#KEY_MIME 指明。对于视频类型而言,这个数据通常是一个压缩后的视频帧。对于音频数据而言,通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频数据,数据类型format type 指定),有时候,一个音频单元对于一个 buffer 而言可能有点宽松,所以一个 buffer 里可能包含多个编码后的音频数据单元。无论 Buffer 包含的是视频数据还是音频数据, Buffer 都不会再任意字节边界上开始或者结束,而是在帧(视频)或者单元(音频)的边界上开始或者结束。除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。
   原始音频Buffer包含PCM音频数据的整个帧,是每一个通道按着通道顺序的采样数据。每一个采样按16Bit量化。
   在 ByteBuffer 模式下,视频数据的排布由 MediaFormat#KEY_COLOR_FORMAT 指定,我们可以通过 getCodecInfo().MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats 获取到一个设备支持的 color format 数组。视频 Codec 可能支持三种类型的Color Format:
   从 Build.VERSION_CODES.LOLLIPOP_MR1 开始所有的视频 Codec 都支持 flexible YUV 4:2:0 
   对于 Build.VERSION_CODES.LOLLIPOP 之前并且支持 Image 类时,我们需要使用 MediaFormat#KEY_STRIDE 和 MediaFormat#KEY_SLICE_HEIGHT 的值去理解输出的原始视频数据的布局。
   键值 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 指明了视频Frame的size。然而,对于大多数用于编码的视频图像,他们只占用了video Frame的一部分。这部分用一个 'crop rectangle 来表示。
   我们需要用下面的一些 keys 从获取原始视频数据的 crop rectangle ,如果 out format 中没有包含这些 keys ,则表示视频占据了整个 video Frame ,这个 crop rectangle 的解释应该立足于应用任何 MediaFormat#KEY_ROTATION 之前。
   下面是在旋转之前计算视频的尺寸的案例:
   从概念上讲Codec的声明周期存在三种状态: Stoped , Executing , Released 。 Stoped 状态是一个集合状态,它聚合了三种状态: Uninitialized , Configured ,和 Error ,同时 Executing 状态的处理也是通过三个子状态来完成: Flushed , Running , End-of-Stream 。
   
                                           
    Executing 状态有三个子状态:Flushed,Running,和End-of-Stream,当我们调用玩 Start() 函数后, Codec 就立刻进入 Flushed 子状态,这个状态下,它持有全部的buffer,只要第一个Input buffer被dequeued,Codec就转变成 Running 子状态,这个状态占据了 Codec 的生命周期的绝大部分。当入队一个带有 end-of-stream标志的InputBuffer后, Codec 将转换成 End of Stream 子状态,在这个状态下, Codec 将不会再接收任何输入的数据,但是仍然会产生output buffer ,直到end-of-Stream标记的buffer被输出。我们可以在 Executing 状态的任何时候,使用 flush() 函数,将 Codec 切换成 Flushed 状态。
   调用 stop() 函数会将 Codec 返回到 Uninitialized 状态,这样我们就可以对 Codec 进行重新配置,当你用完了 Codec 后,你必须要调用 release() 函数去释放这个 Codec 。
   在极少数情况下, Codec 可能也会遇到错误,此时 Codec 将会切换到 Error 状态,我们可以通过queuing操作获取到一个无效的返回值,或者有时会通过异常来的得知 Codec 发生了错误。通过调用 reset() 函数,将 Codec 进行重置,这样 Codec 将切换成 Uninitalized 状态,我们可以在任何状态下调用 rest() 函数将Codec 将切换成 Uninitalized`状态。
   使用 MediaCodecList 创建一个指定 MediaFormat 的MediaCodec。当我们解码一个文件或者一个流时,我们可以通过 MediaExtractor#getTrackFormat 获取期望的Fromat,同时我们可以通过 MediaFormat#setFeatureEnabled 为 Codec 注入任何我们想要的特性。然后调用 MediaCodecList#findDecoderForFormat 获取能够处理对应format数据 Codec 的name,最后我们使用 createByCodecName(String) 创建出这个 Codec 。
   我们也可以使用 createDecoder/EncoderByType(java.lang.String) 函数来创建指定的 MIME 类型的 Codec ,但是这样我们无法向其中注入一些指定的特性,这样创建的 Codec 可能不能处理我们期望的媒体类型数据。

3. android mediacodec有什么方法

Android 用MediaCodec实现视频硬解码
本文向你讲述如何用android标准的API (MediaCodec)实现视频的硬件编解码。例程将从摄像头采集视频开始,然后进行H264编码,再解码,然后显示。我将尽量讲得简短而清晰,不展示那些不相关的代码。但是,我不建议你读这篇文章,也不建议你开发这类应用,而应该转而开发一些戳鱼、打鸟、其乐融融的程序。好吧,下面的内容是写给那些执迷不悟的人的,看完之后也许你会同意我的说法:Android只是一个玩具,很难指望它来做靠谱的应用。
1、从摄像头采集视频
      可以通过摄像头Preview的回调,来获取视频数据。
      首先创建摄像头,并设置参数:

[java] view plaincopy
                     cam = Camera.open();  
cam.setPreviewDisplay(holder);                    
Camera.Parameters parameters = cam.getParameters();  
parameters.setFlashMode("off"); // 无闪光灯  
parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);  
parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);  
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);   
parameters.setPreviewFormat(ImageFormat.YV12);       
parameters.setPictureSize(camWidth, camHeight);  
parameters.setPreviewSize(camWidth, camHeight);  
    //这两个属性 如果这两个属性设置的和真实手机的不一样时,就会报错  
cam.setParameters(parameters);            
宽度和高度必须是摄像头支持的尺寸,否则会报错。要获得所有支持的尺寸,可用getSupportedPreviewSizes,这里不再累述。据说所有的参数必须设全,漏掉一个就可能报错,不过只是据说,我只设了几个属性也没出错。    然后就开始Preview了:


[java] view plaincopy
buf = new byte[camWidth * camHeight * 3 / 2];  
cam.addCallbackBuffer(buf);  
cam.setPreviewCallbackWithBuffer(this);           
cam.startPreview();   
  setPreviewCallbackWithBuffer是很有必要的,不然每次回调系统都重新分配缓冲区,效率会很低。

    在onPreviewFrame中就可以获得原始的图片了(当然,this 肯定要 implements PreviewCallback了)。这里我们是把它传给编码器:

[java] view plaincopy
public void onPreviewFrame(byte[] data, Camera camera) {  
    if (frameListener != null) {  
        frameListener.onFrame(data, 0, data.length, 0);  
    }  
    cam.addCallbackBuffer(buf);  
}  
2、编码

    首先要初始化编码器:

[java] view plaincopy
      mediaCodec = MediaCodec.createEncoderByType("Video/AVC");  
MediaFormat mediaFormat = MediaFormat.createVideoFormat(type, width, height);  
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);  
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);  
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);  
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);  
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);  
mediaCodec.start();  

    然后就是给他喂数据了,这里的数据是来自摄像头的:


[java] view plaincopy
public void onFrame(byte[] buf, int offset, int length, int flag) {  
   ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();  
   ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();  
   int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);  
   if (inputBufferIndex >= 0)  
       ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];  
       inputBuffer.clear();  
       inputBuffer.put(buf, offset, length);  
       mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, 0, 0);  
   }  
   MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();  
   int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);  
   while (outputBufferIndex >= 0) {  
       ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];  
       if (frameListener != null)  
           frameListener.onFrame(outputBuffer, 0, length, flag);  
       mediaCodec.releaseOutputBuffer(outputBufferIndex, false);  
       outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);  
   }   
先把来自摄像头的数据喂给它,然后从它里面取压缩好的数据喂给解码器。
3、解码和显示
     首先初始化解码器:

[java] view plaincopy
mediaCodec = MediaCodec.createDecoderByType("Video/AVC");  
MediaFormat mediaFormat = MediaFormat.createVideoFormat(mime, width, height);  
mediaCodec.configure(mediaFormat, surface, null, 0);  
mediaCodec.start();  

             这里通过给解码器一个surface,解码器就能直接显示画面。
     然后就是处理数据了:

[java] view plaincopy
public void onFrame(byte[] buf, int offset, int length, int flag) {  
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();  
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);  
        if (inputBufferIndex >= 0) {  
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];  
            inputBuffer.clear();  
            inputBuffer.put(buf, offset, length);  
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 1000000 / FRAME_RATE, 0);  
                   mCount++;  
        }  
  
       MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();  
       int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);  
       while (outputBufferIndex >= 0) {  
           mediaCodec.releaseOutputBuffer(outputBufferIndex, true);  
           outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);  
       }  
}  
        queueInputBuffer第三个参数是时间戳,其实怎么写都无所谓,只要是按时间线性增加的就可以,这里就随便弄一个了。后面一段的代码就是把缓冲区给释放掉,因为我们直接让解码器显示,就不需要解码出来的数据了,但是必须要这么释放一下,否则解码器始终给你留着,内存就该不够用了。


好了,到现在,基本上就可以了。如果你运气够好,现在就能看到视频了,比如在我的三星手机上这样就可以了。但是,我试过几个其他平台,多数都不可以,总是有各种各样的问题,如果要开发一个不依赖平台的应用,还有很多的问题要解决。说说我遇到的一些情况:

1、视频尺寸
     一般都能支持176X144/352X288这种尺寸,但是大一些的,640X480就有很多机子不行了,至于为什么,我也不知道。当然,这个尺寸必须和摄像头预览的尺寸一致,预览的尺寸可以枚举一下。
2、颜色空间
    根据ANdroid SDK文档,确保所有硬件平台都支持的颜色,在摄像头预览输出是YUV12,在编码器输入是COLOR_FormatYUV420Planar,也就是前面代码中设置的那样。       不过,文档终究是文档,否则安卓就不是安卓。
    在有的平台上,这两个颜色格式是一样的,摄像头的输出可以直接作为编码器的输入。也有的平台,两个是不一样的,前者就是YUV12,后者等于I420,需要把前者的UV分量颠倒一下。下面的代码效率不高,可供参考。

[java] view plaincopy
byte[] i420bytes = null;  
private byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {  
    if (i420bytes == null)  
        i420bytes = new byte[yv12bytes.length];  
    for (int i = 0; i < width*height; i++)  
        i420bytes[i] = yv12bytes[i];  
    for (int i = width*height; i < width*height + (width/2*height/2); i++)  
        i420bytes[i] = yv12bytes[i + (width/2*height/2)];  
    for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)  
        i420bytes[i] = yv12bytes[i - (width/2*height/2)];  
    return i420bytes;  
}  
      这里的困难是,我不知道怎样去判断是否需要这个转换。据说,Android 4.3不用再从摄像头的PreView里面取图像,避开了这个问题。这里有个例子,虽然我没读,但看起来挺厉害的样子,应该不会有错吧(觉厉应然)。http://bigflake.com/mediacodec/CameraToMpegTest.java.txt


3、输入输出缓冲区的格式
    SDK里并没有规定格式,但是,这种情况H264的格式基本上就是附录B。但是,也有比较有特色的,它就是不带那个StartCode,就是那个0x000001,搞得把他编码器编出来的东西送给他的解码器,他自己都解不出来。还好,我们可以自己加。

[java] view plaincopy
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];  
byte[] outData = new byte[bufferInfo.size + 3];  
       outputBuffer.get(outData, 3, bufferInfo.size);  
if (frameListener != null) {  
    if ((outData[3]==0 && outData[4]==0 && outData[5]==1)  
    || (outData[3]==0 && outData[4]==0 && outData[5]==0 && outData[6]==1))  
    {  
        frameListener.onFrame(outData, 3, outData.length-3, bufferInfo.flags);  
    }  
    else  
    {  
     outData[0] = 0;  
     outData[1] = 0;  
     outData[2] = 1;  
        frameListener.onFrame(outData, 0, outData.length, bufferInfo.flags);  
    }  
}  

4、有时候会死在dequeueInputBuffer(-1)上面

根据SDK文档,dequeueInputBuffer 的参数表示等待的时间(毫秒),-1表示一直等,0表示不等。按常理传-1就行,但实际上在很多机子上会挂掉,没办法,还是传0吧,丢帧总比挂掉好。当然也可以传一个具体的毫秒数,不过没什么大意思吧。

android mediacodec有什么方法

最新文章
热门文章
推荐阅读