在Android 倍速压缩视频时长中,实现了针对完整视频进行倍速压缩的功能。用户反馈说,这样的一个倍速视频就和看轨迹回放一样的效果,从头到尾完整播放一遍,体现不出“牛逼”的地方。于是想着针对学员“操作”的地方进行切片,比如打方向盘、挂挡等。当出现这些操作时,对整个视频进行切割,生成若干个片段,然后再拼接到一起。片段衔接处添加转场效果,让切换看起来更丝滑。这样处理后的视频,可能就是一个富含学员“精彩操作”的视频,学员查看和分享的欲望可能就更强烈了。基于这样的一个需求场景,需要做的事情就确定了:
- 对完整视频进行切片,分割成若干个片段;
- 将片段结合转场效果合并到一起,生成最终视频。
切片
经过产品讨论,目前设定的操作切片逻辑如下:
- 进入项目的三秒内;
- 离开项目的三秒前;
- 拉手刹;
- 停车;
- 打方向盘超过360度;
- 侧方接触36线时。
生成的切片无非就是若干个时间片段(相对时间)。基于 VideoProcessor 的 VideoDecodeThread 类,做了相关修改:
1 | public class VideoDecodeThread extends Thread { |
VideoDecodeThread 类主要做的就是将视频解码,绘制到 Surface 上。针对倍速、截取视频长短,无非就是控制绘制的逻辑。代码里有 doRender 参数的设置,为 true 时才会绘制。
添加了一个List<Pair<Long, Long>> mCutSegments
的参数,由外部传入,就是需要切片的时间段,示例如下:
当 info.presentationTimeUs 处于时间片段内时,doRender 才设置成 true,才会执行绘制逻辑。
经过测试确实可以切片了,但是还有一个问题:视频切片的长度,并不是近似等于时间片段的总时长。使用示例数据,应该是生成 11 秒上下的视频,用两个测试视频进行测试,一个视频最终只有 9 秒,另一个视频却有 13 秒。
最终定位到问题:decode 是解码进行 Surface 绘制,但是真正写 mp4 文件的还是 encode 类,encode 时的 presentationTimeUs 不是严格对齐的。
1 | public class VideoEncodeThread extends Thread implements IVideoEncodeThread { |
mMuxer.writeSampleData 才是对视频文件的最终写入。所以参数 info 的 presentationTimeUs 至关重要,它会控制视频的整体时长。但是针对视频分片,只能用解码的 presentationTimeUs 进行处理,索性便将解码的 presentationTimeUs 直接传到编码这边直接用了:
1 | if (time != -1) { |
如此操作之后,生成的视频长度便符合预期了。
转场
Android 对视频添加转场效果,之前没什么经验,搜索出来的都是针对两个视频添加转场效果。在当前场景中,这只能作为最下下下策方案。不然生成若干个小视频,再两两拼接,这耗时可就太久了,最好的就是在写 mp4 时直接就生成好。再搜索一番,发现可以利用 OpenGL 添加转场效果。
VideoProcessor 库实现视频编解码正好利用到了 OpenGL:
1 | public void drawFrame(SurfaceTexture st, boolean invert, float percent) { |
研究了若干文章,都是针对两个图片帧实现的效果。GLTransitions这个项目实现的效果可太酷炫了,可要怎么移植到 Android 中呢?曾经妄想能直接找到对应的着色器直接使用,毫无疑问全部失败。经过一阵无厘头的摸索之后恍然醒悟:需要自己做动画百分比,让当前帧达到一个预想的中间态。就和安卓中最普通的动画一样。
所以通过时间戳计算出一个 percent:
1 | for (int i = 0; i < mCutSegments.size(); i++) { |
然后在 drawFrame 时,传入到 OpenGL 中。那么 OpenGL 如何自定义参数呢?
1 | private static final String VERTEX_SHADER = |
参照着代码中定义的参数,定义了一个 vPercent 的参数,并在 main 方法中使用了起来:使用 rgb 乘以一个类似 alpha 的系数,那么最终的效果就是慢慢变暗,再慢慢变亮。
先定义变量:
1 | private int mPercent; |
然后映射属性:
1 | mPercent = GLES20.glGetUniformLocation(mProgram, "vPercent"); |
对 vPercent 进行赋值:
1 | vPercent[0] = percent; |
最终使用到 OpenGl:
1 | GLES20.glUniform2fv(mPercent, 1, vPercent, 0); |
最终实现效果如下:
小结
通过时间戳,控制 surface 的渲染,从而实现分片。同时在绘制 surface 时,结合 OpenGL 的能力,设置透明度,从而实现最简单的转场。分片逻辑好说,比较清晰,无非就是怎么通过时间片段完善代码。但是 OpenGL 的研究真的是熬死人了,它是一个很系统化的知识点,能力也确实强大。但是又没有很宽松的时间从头开始学习,只能边研究边实现功能,很多东西都是囫囵吞枣,一撇而过。不过好在研究出来了,至少最简单的转场功能是实现了。后续若想做更酷炫的转场动画,相信也是能搞出来的!