0x00 引言
在iOS里,我们可以利用AVAssetExportSession来对视频进行编辑合成并输出到文件, 使用方法也很简单
1 | AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetPassthrough]; |
使用调用 exportAsynchronouslyWithCompletionHandler 方法并设置好回调即可.
但今天显然不是想讲API的使用方法.
0x10 问题
当前所做的项目允许导入相册的视频并对视频进行裁剪, 然后有一个确认页面让用户检查待发布的视频, 并允许选择封面, 封面的选择可以从视频里选择一帧, 此乃问题背景.
裁剪所用的方法也是使用AVAssetExportSession, 并设置好timeRange属性来指定裁剪范围,并使用exportAsynchronouslyWithCompletionHandler执行导致.
导出后,在封面选择页使用AVPlayer组件控制
1 | AVURLAsset *videoSource= [AVURLAsset URLAssetWithURL:assetURL options:options]; |
整体其实看上去也很简单,但问题是, 当导入的视频为坚屏拍摄时,在封面选择的界面里显示的封面是90度旋转的.
正常是坚屏的:
选择封面时变成了横屏:
这很明显是不合理的.
0X20 分析
0x21 视频源分析
首先从现象中容易得知,原视频是没问题的,因为在裁剪页里AVPlayer是好好地支持了这个视频的坚屏效果的, 而到了封面选择才变得有问题, 那问题多半是出裁剪导出视频这里.
但为了谨慎起见, 还是先从源视频开始分析. 直接手机 airdrop 视频到电脑上, 虽然导出的视频可能为mov格式,但依然可以使用ffmpeg读取视频的信息
1 | ffmpeg -i IMG_0392.MOV |
其中, 视频包含了一个side data,里面有一个displaymatrix信息
displaymatrix: rotation of -90.00 degrees
从字面上理解, 这是逆时针旋转90度的意思, 开始我还很郁闷, 明明是竖屏,为什么会是旋转了90度? 现在姑且不讨论这个问题, 我们知道了这个显示矩阵里包含了视频旋转的信息, 那既然最后封面选择的视频是逆转了90度,那是不是导出的视频的显示矩阵少了东西?
马上导出了使用AVAssetExportSession导出的视频,现在已经变成了mp4了,再次使用ffmpeg来读取信息
结果跟想象中的并不一样, 信息还是存在的.
我尝试使用播放器(SPLAYERX,射手)直接播放原视频与导致后的视频, 结果也是一个是竖的,一个是横的,难道刚看的信息没用?
0x22 变换矩阵
抱着刚刚的想法, 改用直接使用代码来获取视频的旋转方向:
1 | NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; |
这方法是直接从AVAsset中读取视频轨, 然后获取视频的变换矩阵,所谓的变换矩阵以前线代也有教,简述如下:
一个二维图形的变化,其坐标可以表示为(X,Y),为了统一,可以使用三维坐标表示(X,Y,Z),而Z维度恒为1.
变换矩阵表示如下:
1 | |a|b|0| |
当二维图形坐标与变换矩阵一并运算后,表示为
1 |
|
如此,原图形的坐标就被统一变换为一个新的坐标集.从结果也可以看出,通过控制 a,b,c,d,tx,ty ,就可以实现图形的变换.
如上面方法,怎么判断矩阵是旋转90度(不涉及位移), 将上面的a,b,c,d代入原坐标,变成
[0X+(-1)Y+0, 1X+0Y+0, 1] ==> [ -Y, X,1]
即X与Y调转,也就是旋转了90度,至于前面-Y的-号,即表示了旋转的方向.
同理,180度为
[(-1)X+0Y+0, 0X+(-1)Y+0, 1] ==> [ -X, -Y, 1]
X与Y都互相反转.
背景讲完,回到问题本身, 从测试结果来看, 原视频被认为是旋转了90度, 而裁剪视频被认为是0度*(即无旋转), 这也是与看到的现象是一致的.
0x23 为什么是90°
回到上面里提及到的一个问题,为什么源视频的矩阵信息是旋转了90°,但从结果上来看,结果却是正确的?
产生这一个疑问的原因是,从我(或我看到)的手机使用习惯来看,我们大都习惯竖屏使用,把坚屏理解为标准情况(无任何旋转位移变换),但系统并不是这样子认为的.事实上,在iOS里, 横屏且HOME在右边的状态,才是它理解上的标准情况.旋转角度情况如下图所示.
以上为一般使用自带相机拍出来的情况,因为相机都会带有使用到重力感应,这部分的数据会成为旋转角度的依据.下面还会讲到另一种情况.
0x30 解决
0x31 解决竖屏视频
既然知道问题出在了视频裁剪导出上,且是由于转置矩阵里的信息发生了变化(从-90°变成0),也就是相当于源视频逆时针转了90°,从结果上来看,就是视频”变横了”, 与预期不符. 那我们能不能把裁剪视频的信息里加入源视频的旋转信息呢,答案当然是可以的,使用到的还是AVAssetExportSession, 但需要额外设置其videoComposition属性.
videoComposition属于AVVideoComposition类型, 可以理解为视频的控制信息,可以控制视频导出时的我,利用它我们可以让视频导出时加入旋转90度的信息, 构建videoComposition的过程如下.
1 | - (AVMutableVideoComposition *)getVideoComposition:(AVAsset *)asset { |
部分含义可见注释, 其中最关键的部分为
1 | ...... |
前者是针对坚屏的视频交换高宽,从实际效果来看就是横变竖了; 后者,是对导出视频加入转置矩阵的旋转信息,而这个信息与源视频保持一致.
如此,对于竖屏拍摄的视频重新导出的裁剪视频, 在封面选择页里就能恢复为竖屏的形态了.
延伸一下, 对于
videoComposition.frameDuration = CMTimeMakeWithSeconds( 1 / videoTrack.nominalFrameRate, 600);
这一行, 它表示的是帧率, 而videoTrack.nominalFrameRate可以获取到源视频轨的帧率,而CMTimeMakeWithSeconds定义如下:
1 | CMTime CMTimeMakeWithSeconds( |
那么,1/videoTrack.nominalFrameRate获取到的,就是第一帧所处的时间, 而第二个参数,则指定了导出视频的帧率.
其中,600并非一个常数,只是用来指代帧率而已,普通视频一般都是600, 对于 AVAsset,可以通过获取其duration的timescale属性.
0x32 再遇问题
是的, 世事往往没有这么顺利,在我朝,有一个叫微信的东西,由它拍出来的小视频导入时,经过上面的纠正后, 重新导出的视频直接就黑了, 直接查看视频数据,发现导出视频里根本没有图像相关的信息,这表示在重新导出时,我们可能把渲染的范围设为视频图像外, 或者很简单,压根就没把图像给转换出来.
这就很尴尬了,难道上面的推断都错了吗?我想信应该不是的,应该是视频本身与使用自带相机拍出来的有差别.
重走上面的流程, 首先使用播放器直接播放微信的视频,结果发现无论是使用竖屏拍还是横屏拍,最终视频都是横的, 其中,在微信小视频录制中,使用竖屏录制,出来的效果是这样的
而得到的转置矩阵,表示它还是-90°;
而使用横屏录制(home在右边)干脆就反转了180度(以地面为标准)
矩阵表示旋转已经到了180°.
可以看出,矩阵表示与播放器效果一致.
从以上关系, 可以得出, 以微信拍摄的视频最后统一都是横屏的(可以理解为宽>高), 转置矩阵里的旋转关系是正确的,那导致出来黑屏的原因是什么呢?上面说到videoComposition的的配置中有两个关键的部分,一个是对于竖屏视频, 需要交换高宽,在这一点上还是成立的; 第二点是转置矩阵的复制(从源视频), 问题很可能出在这里.
对于坚屏拍摄的视频,查看其矩阵值如下
而使用自带相机拍摄的视频,其矩阵为
也就是旋转90度,这没问题.问题出在旋转之后, 图像脱离了可渲染范围.
首先我们获取到源视频的naturalSize
CGSize videoSize = videoTrack.naturalSize;
看到此时视频的宽高已经与我们的理解是不一样的,这点用自拍的相机拍也是这种效果,所以才需要交换宽高,但旋转后的效果却是不一样的.这里我们先引入一个 AnchorPoint 的概念来说明一下旋转的过程(我没搜索到其他地方有类似的描述, 是我依据以往的图像变换的经验和现象总结出来的,不一定存在这东西).
微信拍出来的视频之所以为黑, 是因为在旋转后离开了可显示区域, 所以才为黑,其原因,我观察推测,其anchorpoint是在视频的左上角(0,0)位置, 旋转方式如下(不考虑宽高交换),自蓝向绿色变换.
知道这个原理之后,我们要修复就很简单的,只需要把视频再作一个平移就可以了,此时需要用到一个tx属性,详情参考下面代码.
按照这个原理来处理横屏视频, 因为横屏的视频是转了180度,anchorpoint还是为(0,0),那它们就是转到了下图的位置
这时,需要同时纠正tx与ty值, 构建矩阵如下:
CGAffineTransformMake(-1,0,0,-1,videoSize.width,videoSize.height)
如果遇到其他的情况也可以用相同的办法处理.
0x33 优化
对3.1节的代码进行重新优化后, 完整代码如下:
1 | - (AVMutableVideoComposition *)getVideoComposition:(AVAsset *)asset { |
最关键的还是针对不同的旋转角度重新构建了转置矩阵,以适应不同视频的旋转情况,并同时兼容了来自不同的视频.
0x34 压缩
在创建 AVAssetExportSession 时,有时为了加快导出,将presetName设置为AVAssetExportPresetPassthrough
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetPassthrough];
但如果在上面的场景下,设置了videoComposition时,如果使用了AVAssetExportPresetPassthrough,是不会按我们要求地来修改导出视频的.
其定义说明如下:
1 | /* This export option will cause the media of all tracks to be passed through to the output exactly as stored in the source asset, except for |
它只会把源视频数据里的所有的音视频轨都输出去,而不会管videoComposition. 需要其生效,presetName需要设置成其他,如AVAssetExportPresetHighestQuality等.
0x40 总结
视频图像变换操作需要注意几个部分
- 图像是否在可显示区域(renderSize)
- 视频的旋转是通知定义矩阵来实行的
- 视频的正位为横屏(HOME在右),逆时针为正