视频编辑总结

最近公司在做视频编辑相关的功能,现在将做项目过程中遇到的问题总结如下:

1.视频格式概念
      平常所见的视频格式比如MP4,AVI,MKV,MOV,FLV等是视频的包装格式。是将音频部分与视频播放包转起来。也就是通常英文里面说的container。而每一种视频包装格式,里面视频与音频的编码又是多种多样的。
2.视频轨道
    我们通常所说的视频压缩就是在视频轨道里面做的,比如H263,H264,H265,VP8,VP9,这些都是视频压缩编码格式,目的是将一帧帧原始视频帧数据,通过一定的算法(包括帧内压缩,帧间压缩),将原始很大的数据量,压缩到很小。压缩这里涉及到I,B,P帧的概念,后面有需要了解.

    I B P帧大致介绍:

1、I帧

I帧又称帧内编码帧,是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频序列中的第一个帧始终都是I帧,因为它是关键帧。

2、P帧

P帧又称帧间预测编码帧,需要参考前面的I帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。与I帧相比,P帧通常占用更少的数据位,但不足是,由于P帧对前面的P和I参考帧有着复杂的依耐性,因此对传输错误非常敏感。

3、B帧

B帧又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是对解码性能要求较高。动漫中用到的这种编码方式比较普遍,表现在代码层面,就是通过MediaExtractor将视频帧分离出来之后,时间戳不是按照时间顺序排列。

3.音频轨道
    音频部分目前主要是AAC压缩格式,目前项目中没有涉及音频编码与解码,只是将音频轨道读取出来之后,进去裁剪,重新写回文件
4.视频编解码方案的选择
    目前一般市面上移动端(android)的视频编辑方案无外乎两种
    1.利用android系统提供的MediaCodec,MediaExtractor,MediaMuxer,进行硬件编解码,优势是系统支持,不需要额外的库,可以很好的利用硬件,速度比较有优势,缺点是从API16开始支持(部分类从API18开始支持),覆盖不了低版本,同时不同厂家对于这部分的硬件实现可能有部分差别,会有适配问题,还有系统接口使用不是很方便,处理流程稍微复杂一些。
    2.再有就是用ffmpeg,这个库是一个非常优秀的库,不光在android平台,其他平台都有,优势是支持格式很多,由于库比较成熟,调用非常简单,缺点是默认的ffmpeg是软解码,速度非常慢,需要单独做硬件优化才行。
    考虑到在硬件优化方面没有经验,所以最终选择了第一种方案,也就是利用android系统提供的类进行编解码。
5.渲染字幕方案
    视频解码器将视频数据解码后,通过android中的surfacetexture,渲染到opengl的纹理中,然后在渲染视频的过程中,将字幕文字转化为纹理,同时渲染到opengl窗口中,这里用了离屏渲染,没有真正渲染到屏幕上,而是渲染到GPU,可以提高渲染效率。最后通过编码器读取Suface的数据,将渲染好的视频,重新进行压缩编码。这样就完成了字幕渲染的过程。完整的流程之后,会有单独介绍。
6.解码渲染编码整体流程叙述

    MediaExtractor 处理
        MediaExtractor主要的作用是将音频轨道与视频轨道分离,分别从不同轨道读取帧数据,然后将数据送入解码器。
        下面是使用的基本流程
 MediaExtractor extractor = new MediaExtractor();
 extractor.setDataSource(…);//设置视频地址(可以在线地址,本地地址)
 int numTracks = extractor.getTrackCount();//得到轨道数
 for (int i = 0; i < numTracks; ++i) {
   MediaFormat format = extractor.getTrackFormat(i);
   String mime = format.getString(MediaFormat.KEY_MIME);
   if (weAreInterestedInThisTrack) {//如果需要这个轨道,则直接selectTrack选中
     extractor.selectTrack(i);
   }
 }
 ByteBuffer inputBuffer = ByteBuffer.allocate(...)
 while (extractor.readSampleData(inputBuffer, ...) >= 0) {//选中之后,就可以进行读取数据到buffer中
   int trackIndex = extractor.getSampleTrackIndex();
   long presentationTimeUs = extractor.getSampleTime();
   ...
   extractor.advance();//步进到下一个帧
 }

 extractor.release();//释放资源
 extractor = null;
    Mediacodec 工作流程
    下图是编码解码器通用的流程。不管是编码还是解码,首先有一个输入缓冲区inputbuffer,和outputbuffer。和一个(编码器或者解码器)。

    下面分别叙述编码过程与解码过程
  •     解码
       我们从配置好的mediacodec中获取inputbuffer,然后将从mediaextractor中读取到的数据,写入inputbuffer。inputbuffer是用队列实现的,我们通过dequeueInputBuffer获取到inputbuffer的索引,然后通过索引得到inputbuffer,然后写入数据。inputbufer队列中有数据后,codec开始工作,进行异步解码操作,最后将解码后的数据输出到outputbuffer的队列中,接着我们从outputbuffer中取出解码后的数据输送到surface中,这一步(从outputbuffer到surface)是系统帮我们完成,通过releaseOutputBuffer(int index, boolean render)第二个参数指定,是否需要传输到surface上。这样我们就完成了解码的操作。

  •   渲染
       在上一步中,解码后的数据通过传递给了SurfaceTexture,然后初始化好opengl的环境,将SurfaceTexture中的视频数据作为纹理映射给了opengl渲染,同时如果需要添加字幕,我们也将字幕文字通过Bitmap的drawtext方法,将字幕生成Bitmap,然后也将字幕的Bitmap,作为纹理叠加映射到opengl中。这里通过opengl的操作,可以做到很多特效,比如通过shader,做一些特殊的滤镜,也可以对视频进行空间三维变换,这里想象空间很大,可以做的事情也很多。
  • 编码

mediacodec的编码器支持从surface中读取数据,然后进行输入编码器,比直接将数据写入inputbuffer快很多。这里我们调用swapbuffer将离屏渲染的视频和字幕,交换到mediacodec创建出来的inputsurface上,然后再次通过mediacodec编码器,最后从编码器的outputbuffer中将数据写入混合器.

  •  音视频混合写入文件

上一步将视频编码后,获取到了编码后的视频数据,然后,我们利用mediamuxer将数据写入指定的视频轨道。在这里视频处理的部分结束了。对于音频部分,由于目前音频目前不需要编辑,所以把直接用mediaextrator读取的音频数据,直接通过mediamuxer写入文件中。

下面是写入文件的通用流程

 MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
 // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
 // or MediaExtractor.getTrackFormat().
 MediaFormat audioFormat = new MediaFormat(...);
 MediaFormat videoFormat = new MediaFormat(...);
 int audioTrackIndex = muxer.addTrack(audioFormat);
 int videoTrackIndex = muxer.addTrack(videoFormat);
 ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
 boolean finished = false;
 BufferInfo bufferInfo = new BufferInfo();

 muxer.start();
 while(!finished) {
   // getInputBuffer() will fill the inputBuffer with one frame of encoded
   // sample from either MediaCodec or MediaExtractor, set isAudioSample to
   // true when the sample is audio data, set up all the fields of bufferInfo,
   // and return true if there are no more samples.
   finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
   if (!finished) {
     int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
     muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
   }
 };
 muxer.stop();
 muxer.release();
遇到过的坑:
1.非固定帧率
    解决方案:最早时候,是根据读取的帧率算出,每一帧的时间戳,这样导致帧率可变时候,视频时快时慢。最后还是将原视频中的时间戳,直接写入编码后的视频帧中。
2.B帧视频时间戳裁剪问题
    解决方案:b帧视频最关键的问题是,由于时间戳不是按照时间顺利来的,这样导致mediamuxer写入的时候,不支持乱序写入视频帧。刚开始尝试手动调整视频帧的顺序。但是写入后,视频播放不对。最后猜想,既然b帧视频播放正常,那么说明解码后渲染出来时间戳已经是正确的了。于是考虑B帧视频裁剪的时候,也通过解码器,渲染结束后,再写回文件。
3.seek操作只能定位到关键帧
    通过解码后,步进的方式,进行裁剪
4.视频写入过程中断,则音频必须写入一帧再推出,不然报错
5.视频方向读取
    (1).读取问题(有些版本的android,读取不到),MediaMetadataRetriever通过这个读取
    (2).exoplayer预览问题
        读取不到视频方向,但是播放器有回掉,手动将预览页面旋转。
6.MediaExtractor的seek操作,如果seek点是0的时候,有些手机不支持这个参数SEEK_TO_PREVIOUS_SYNC,会native crash
7.预览精确seek播放
    解决方案:
    通过调研,找到了google官方写的exoplayer,可以做到精确seek

发表评论

电子邮件地址不会被公开。 必填项已用*标注