iOS视频合成预览预处理

不久前在做一个视频处理的Demo,找时间抽空总结一下遇到的问题。

image7

如上图,Demo为一般视频编辑App中常见的功能。从相册选择多段视频资源,并在一条时间轴上显示相应时间点的缩略图视频帧,并可以添加滤镜与切割视频片段。

AVCompostion

AVComposition 继承自AVAsset,将来自多个基于源文件的媒体数据组合在一起显示,或处理来自多个源媒体数据。iOS在处理多个视频资源合成都用这个方案来先预合成一段新视频,预合成过程不会有任何耗时。预合成多段视频资源后,体现为一个新的AVAsset,可以使用常规的 AVPlayer做播放。

qPHp8p

上图可表示为一个 AVComposition,导入了2个视频素材资源,视频轨道(Track)添加了2段视频,音频轨道(Track)添加了视频素材资源A的音轨,所形成的一个新的AVAsset

通常我们使用

1
2
3
4
5
6
7
8
9

AVMutableComposition *composition = [AVMutableComposition composition];

//视频轨道
AVMutableCompositionTrack *videoCompositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];

//音频轨道
AVMutableCompositionTrack *audioCompositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

来创建2条(视频与音频)常用的轨道,把视频原来资源合并上去。

addMutableTrackWithMediaType:preferredTrackID: | Apple Developer Documentation

1
>preferredTrackID

The preferred track ID for the new track. The system generates a unique ID if the value you specify isn’t available. If you don’t need to specify a preferred track ID, pass kCMPersistentTrackID_Invalid, and the system generates an appropriate identifier.

API中的ID如果不需要准确标识则用系统默认即可。

注意,如果添加过多的 Track 好像会导致 Export时候有问题,至于上限多少好像没有提及。一些视频编辑软件可以添加背景音乐等的,就是多添加一条音轨来合成背景音乐的。

AVAssetTrack

此时 AVComposition(下称预合成视频) 虽然添加了两条 Track,但是并没有往里面添加视频资源,我们会先用以下API先获取视频资源中的音视频 AssetTrack,才能往 预合成视频 添加资源

1
2
3
4
5
6
7

//素材的视频轨
AVAssetTrack *videoAssetTrack = [[AVAsset(视频资源) tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0];

//素材的音频轨
AVAssetTrack *audioAssertTrack = [[AVAsset(视频资源) tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0];

此处会有坑,上面写法只是出于简单,默认拿Asset相应Type的0号Track,但实际上是需要通过遍历方法来获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:@"tracks"] completionHandler:^{
AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:nil];
if (status == AVKeyValueStatusLoaded) {
// Asset is ready now
NSLog(@"Load Success");
}
//异步读取Asset消息
//Video
{
NSArray *arr = [asset tracksWithMediaType:AVMediaTypeVideo];
for (AVAssetTrack *track in arr){


}
//...
}

//Audio 亦如此
}];

网上基本写法为只通过 tracksWithMediaType 获取 asset 的某个Type的所有轨道。但实际上,保险起见需要异步加载Asset资源,在success情况下在遍历该Asset的轨道。另外,遍历AssetTrack时候,需要判断是否该asset有这个类型的轨道。之前由于遍历了一些由图片生成的视频,由于没有音频轨道,在创建预合成视频时候,添加了一个空时间音轨,出现了一些黑屏Bug。

讲获取到的正确的AssetTrack加入到 预合成视频 的轨道中:

1
2
3
4
[videoCompositionTrack insertTimeRange:tR ofTrack:videoAssetTrack atTime:lastVideoCMTime error:&videoError];


[audioCompositionTrack insertTimeRange: tR ofTrack:audioAssertTrack atTime:lastVideoCMTime error:nil];

tR该视频资源中,需要插入的时间范围,用 CMTimeRange 来创建一个Range范围; atTime为需要插入到 预合成视频 中轨道的时间。多段视频资源,这个 Time 需要累积下去,使用 CMTimeAdd 叠加

CMTime vs Double

CMTime这个概念比较复杂,AVFoundation中基本都会用这个来描述时间概念。为什么不用Double呢?

浮点数Double:

  • 描述不准确
  • 累积误差放大

CMTime:

  • 系统数据结构抹除误差
  • 描述准确
  • 对齐业界对视频时间描述 (FFMpeg)

使用两个Int来表达计算机无法准确计算的Float值

所以,AVFoundation中描述时间会用 CMTime。CMTime概念也是比较苦涩,这边我也只能简单描述。

1
2
3
4
5
6
7
8
9
10
 typedef struct

CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;

} CMTime;

CMTime CMTimeMake(int64_t value, int32_t timescale);

CMTime 定义如上。CMTimeValue和CMTimeScale分别是64位和32位有符号整型变量,是CMTime元素的分数形式。CMTimeFlags是一个位掩码用以表示时间的指定状态,比如判断数据是否有效、不确定或是否出现舍入值等。CMTime实例可标记特定的时间点或用于表示持续时间。

最常见的方法是使用CMTimeMake函数,指定一个64位的value参数和32位的timescale参数,比如,创建一个代表5s的CMTime表达式有下面几种不同的方式:

1
2
3
CMTime t1 =CMTimeMake(5, 1);
CMTime t2 =CMTimeMake(3000, 600);
CMTime t3 =CMTimeMake(5000, 1000);

使用CMTimeShow函数将这些值打印到控制台会出现如下结果:

1
2
3
{5/1 = 5.000}
{3000/600 = 5.000}
{5000/1000 = 5.000}

注:在处理视频内容时常见的时间刻度为600,这是大部分常用视频帧率24FPS、25FPS、30FPS的公倍数。音频数据常见的时间刻度就是采样率,比如44 100(44.1kHZ)或48 000(kHZ)。

Apple官方推荐

根据CMTime的定义,其中TimeScale代表把一秒分隔成多少份 Unit,而Value代表当前有几个 Unit

1
Thus if the timescale is 4, each unit represents a quarter of a second; if the timescale is 10, each unit represents a tenth of a second, and so on. 

例如官方文档中说,如果

1
2
3
4
timescale = 4 //代表每秒被分隔成4份
unit = 0.25s // 每个unit代表0.25s
CMTime(3,4) = 0.75s //由3个Unit构成,代表0.75s
//3 / 4 = 0.75

根据某些其他Blog的记载这可能是最详细的CMTime教程,官方文档 曾经出现过推荐把 timescale 设置成 600 理由是 600 是所有帧率的最小公倍数

1
You frequently use a timescale of 600, because this is a multiple of several commonly used frame rates: 24 fps for film, 30 fps for NTSC (used for TV in North America and Japan), and 25 fps for PAL (used for TV in Europe). Using a timescale of 600, you can exactly represent any number of frames in these systems.

但是目前官方文档2019年2月已经没有这句话了,可能是帧率范围变大了

所以,如果把timescale写成小数是有问题的。

如上,则可以合成一段新的预合成视频了

位置描述

image9

如Demo图与上图,如果用户再某个地方按了分割视频,如何描述每一段视频信息以及相对位置描述呢?

由于一开始预合成视频是由3段视频资源合成的一段,没分割之前并不会存在位置描述,从头到尾,时间就是3段视频的总时间,所以描述起来没什么问题。

如果我们在某个地方,例如上方红线出进行切割,在上图看出,分割处为原视频资源中的片段2,某个时间点分割的。分割后,在Demo时间轴上可以看出有2段视频。此时,其实AVComposition已经分成2个预合成视频资源了,切割时候,需要创建一个跟切割点处一模一样的 Model, 储存原来的预合成视频,例如我创建了一个 Model 来记录这些信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface MovieFrameInfoLayout : NSObject

@property(nonatomic,strong)NSString *frameVideoSourceID;

//当前片段唯一标识码,用于调整顺序后一一对应
@property(nonatomic,strong)NSString *currentClipSourceUniID;

@property(nonatomic,strong)AVAsset *videoSource;
@property(nonatomic,assign)CMTime currentVideoSourceStartTime; //该段视频起始Time
@property(nonatomic,assign)CMTime currentVideoSourceEndTime; //该段视频结束Time

//每次调整位置或者更新时候需要重新赋值调整
@property(nonatomic,assign)CMTime wholeVideoStartTime; //相对于整段视频中起始位置
@property(nonatomic,assign)CMTime wholeVideoEndTime; //相对于整段视频结束位置

//删除移动后用这个标记为,标志播放时间从哪段开始
@property(nonatomic,assign)BOOL shouldFoucusAfterEditing;
@end

videoSource为预合成片段资源。在按下切割时候,创建复制一个一模一样的Model,此时2个Model的 videoSource是指向同一个预合成视频的。

另外,分割点的时间需要通过wholeVideoStartTimewholeVideoEndTime来动态计算;过程如下:

  1. 一开始 预合成视频,选了2个视频资源,分别为 5s 和 10s,在时间轴上,显示为一个视频,且市场为15s(5+10)。数据源中含一个 MovieFrameInfoLayout1 ,MovieFrameInfoLayout1的 currentVideoSourceStartTime =0, currentVideoSourceEndTime =15(对于预合成视频来说)。wholeVideoStartTime = 0,wholeVideoEndTime = 15(对于所有数据源中位置)
  2. 比如在12s按下分割按钮
  3. 此时会把 MovieFrameInfoLayout1 复制出一一份,生成MovieFrameInfoLayout2.
  4. MovieFrameInfoLayout1 与 MovieFrameInfoLayout2 的视频资源还是为上的预合成视频资源
  5. MovieFrameInfoLayout1 的 currentVideoSourceStartTime 0,currentVideoSourceEndTime 变为12s; wholeVideoStartTime = 0, wholeVideoEndTime = 12;
  6. MovieFrameInfoLayout2 的 currentVideoSourceStartTime = 12,currentVideoSourceEndTime = 15s; wholeVideoStartTime = 12, wholeVideoEndTime = 15;

每次分割或者调整顺序(如把MovieFrameInfoLayout2 调整到 MovieFrameInfoLayout1 前 ),都需要重新计算生成赋值每一个 Model中的 wholeVideoStartTime 和 wholeVideoEndTime 时间; currentVideoSourceStartTime 与 currentVideoSourceEndTime 也需要在切割视频时候,根据相对时间来计算。

最终,全局还是只持有一个 AVComposition(预合成视频),Model只是用于储存不同片段(如编辑过程中新增了一些视频片段)信息然后合成新的预合成视频。

滤镜

滤镜方面没啥好说,都是基于 GPUImage 那一套输出,预合成视频本身就是一个 AVAsset,通过一些Filter就可以把视频资源通过GPU画到画布上。

Reference:

addMutableTrackWithMediaType:preferredTrackID: | Apple Developer Documentation

AVCompositionTrack | Apple Developer Documentation

短视频从无到有 (三)CMTime详解

CMTime的含义和取值方法 | Voyager-1 (alanli7991.github.io)

iOS视频合成预览预处理

https://swlfigo.github.io/posts/941782149/

Author

Sylar

Posted on

2021-11-13

Updated on

2021-11-14

Licensed under

Comments