Android使用MediaCodec进行视频编码 & 视频的一些基础概念介绍

  • Post author:
  • Post category:其他

相关笔记:Android MediaCodec简单总结_丞恤猿的博客-CSDN博客

#.视频的一些相关概念简介

1.图像的分辨率   
    描述一张图片中的像素数量,一般用像素宽度*像素高度表示。
    摄像头采集的画面是一堆像素点拼接成的,一个像素点可以理解成组成图像的一个色块,大量的色块合在一起就基本能还原出现实中的画面。每个像素点在存储中都记录了对应的颜色特点,具体大小跟颜色格式祥光,例如RGB格式是占用3个byte。
   
2.视频的帧率(FPS)
    我们看到的视频是一张张画面快速切换,在人眼和视觉神经中形成的动态感受,人们感知那是连续不断变化的。
    上述每一张图片都成为视频的一帧,而视频的帧率指的是每秒播放多少张画面。
    一般分辨率越高、帧率越高,越能还原现实中的效果,不过占用的空间就越大。 
(我们在现实中间看到景象是连续的,而在计算机中所有信息都必须数字化之后才能存储,一切数字化的信息都是离散的。无论多高的分辨率、帧率,在现有结构的计算机中永远是无法完全还原真实的。
    当然啦,非要认真讲得话,根据目前的物理学理论,现实中的东西,也不是连续的,而是由离散的粒子构成的,而构成世界的最小粒子目前认为是夸克。当然啦,物理学理论也在不断的演化发展中,100年后现有理论被推翻也不是没可能。) 
3.视频的码率(bitRate)
    指单位时间内播放的数据量,单位:bit。
    同样的采集分辨率和帧率,一般输出码率越高,压缩后生成的视频越清晰。

#.H264视频流与IPB帧

    H264格式的视频流是直播中比较常用的一种视频流压缩编码格式。
##.为何视频要压缩:
    经过摄像头原始采集的画面,若不经过压缩,直接作为视频的一帧画面存储,最后的数据量将是惊人的。那样的话,一帧分辨率1280*720、RGB颜色格式的画面就会占用1280*720*3byte,每秒30帧,长度1小时的视频将占用空间1280*720*3*30*3600byte,大概200多G吧!
    但是我们平时看的视频绝没这么大,它们都被编码压缩过了,经过压缩后,视频体积将大大减小。
##.
H.264视频流中包含三种视频帧:I、P、B帧
   
I帧:关键帧,保存一帧完整画面。
P帧:
前向预测编码帧,保存帧画面的部分差异信息,结合前面的I帧可以还原出完整图像。
B帧:
双向预测内插编码帧,保存帧画面的部分差异信息,结合前面的I帧/P帧以及后面的P帧可还原出完整图像。
##.DTS、PTS
    每一帧有自己的解码时间戳DTS、展示时间戳PTS, 两种时间戳都是由编码器编码时为每一帧生成的。
DTS:解码时间戳DTS控制解码时各帧的先后顺序,根据各个帧的依赖关系,由编码器来计算并设置;
PTS:展示时间戳PTS,控制播放时该帧应该何时展示(以开始播放时刻为0,相对于开始时间),
          这个应该在画面输入时,计算好PTS,并为输入帧设置上,对应的输出帧PTS会与输入帧一致。

#.MediaCodec视频编码

1.根据需要的输出视频流格式正确设置编码器的配置信息
需要设置的主要就是上面所讲的信息,分辨率、帧率、输出码率等。
另外一个关键信息就是对输出关键帧设置:
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.frameRate);//输出帧率
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.iFrameRate);//设置关键帧的频率:多少秒一个关键帧
编码器输出帧率frameRate跟输出帧的展示频率并无太大关系,因为展示时间是PTS决定的;
编码器输出帧率frameRate主要是跟关键帧时间间隔iFrameRate一起决定了自动触发模式下,
       平均多少输出帧包含一个关键帧(I帧),理论上,设置好这两个参数后,
       每frameRate*iFrameRate就会有一个关键帧。
       自动触发模式下,关键帧的触发并不严格按照iFrameRate时间间隔,而是按照输出帧数量来触发的。
       例如,若输入频率定于编码器输出帧率,则实际平均需要更长时间才能达到指定的输出帧数,触发一次关键帧。
当需要时,也可以调用先关API,手动来触发关键帧的输出:
if (Build.VERSION.SDK_INT >= 23) {
       Bundle params = new Bundle();
       params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
        mMediaCodec.setParameters(params);
}。
2.一般会用编码器创建一个输入Surface或为编码器设置一个输出Surface,在这里Surface充当着数据缓冲区的角色。
   使用Surface可以提高编/解码器的性能,Surface直接使用native视频数据缓存,没有映射或复制它们到ByteBuffers,这种方式会更加高效。
3. 控制流程
考虑到版本兼容性,一般采用同步方式来进行编码操作,也就是一次输入操作和一次输出操作按次序轮流执行。
3.1在进行输出操作时,要注意设置正确的PTS。并在需要结束输入时发送带有End-of-Stream标识的输入帧,可调用API:
mEncoder.signalEndOfInputStream();
3.2处理输出操作时,注意使用完输出缓冲区后要及时释放;
另外在bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED时,要及时做相应处理。例如录制本地视频时,此时要为MediaMuxer添加轨道信息。
正常情况下,编码开始后,首次获取到第一个有效输出bufferIndex(非-1)会是此值

#.处理输出缓冲区数据的代码分析,见注释

            //缓冲区信息,可从通过该info获取输出帧的PTS、数据大小,或判断是否为最后一帧等
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            //获取一个可用输出缓冲区的索引,若返回-1表示当前无可用的输出缓冲区
            //第二个参数timeoutUs为等待时长,线程会在此阻塞,一直等到有有效的索引值返回或超时
            //若timeoutUs<0,表示无限期等待;若timeoutUs=0,会立马返回,当然不一定是返回有效的索引
            int bufferIndex = mEncoder.dequeueOutputBuffer(info, 0);
            //获取输出缓冲区队列,其实就是个ByteBuffer数组
            ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers();
            if(bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //输出格式已经更改,后继会按照新的输出格式来输出
                //正常情况下,编码开始后,首次获取到第一个有效输出bufferIndex(非-1)会是此值
                //在此时可以获取到编码器输出格式,并做相应处理
                MediaFormat mediaFormat = mEncoder.getOutputFormat();
                //...........做相应处理.............
            } else if (bufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //输出缓冲区已更改,需要获取新的输出缓冲区
                outputBuffers = mEncoder.getOutputBuffers();
            } else if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                //bufferIndex=-1,表示当前无有效输出缓冲区
                //...........做相应处理.............
            } else if (bufferIndex < 0) {
                //其它bufferIndex<0的情况
                //...........做相应处理.............
            } else if (bufferIndex >= 0) {
                //返回了有效的输出缓冲区
                ByteBuffer data = outputBuffers[bufferIndex];
                //输出缓冲区内有数据
                if(data != null && info.size > 0){
                    //根据输出数据的偏移位置和大小,设置ByteBuffer上的可读写范围
                    data.position(info.offset);
                    data.limit(info.offset + info.size);
                    //...........做相应处理.............
                }
                //释放输出缓冲区
                mEncoder.releaseOutputBuffer(bufferIndex, false);
                //到达最后一个数据输出帧
                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    //...........做相应处理.............
                    break;
                }
            }

版权声明:本文为u013914309原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。