最初下载了几个比较好用的编辑软件体验,大都功能比较完善,体验下来发现分为两大模块
- 自定义编辑模式
- 模板编辑模式
自定义编辑模式,这里面包含的功能比较多,大部分的实现都能找到单独的例子,难点在于怎么把所有的这些功能组合起来,如何编辑,如何预览。
- 视频拼接
- 视频裁剪
- 视频排序
- 视频分割
- 视频旋转
- 视频变速
- 删除视频
- 调整画幅
- 调整音量
- 画中画
- 滤镜
- 视频转场
- 视频倒播
- 背景音乐
- 配乐
- 字幕
- 保存草稿
- 重做功能
- 导出视频
模板编辑模式,即为用户提供「模板视频」,用户只需要选择视频或者图片,便可创作出与「模板视频」有同样编辑特效的同款视频,实现「一键编辑」。
1、技术选型
在iOS平台上,能够实现上面的自定义编辑功能的技术有不少。比如AVFoundation、GPUImage、FFMPEG,本文主要基于AVFoundation来实现。
主要考虑的几个点:
1、使用AVFoundation可以很方便预览,使用AVPlayer来做编辑预览
2、使用AVFoundation性能有保障,后续做优化比较容易,接口比较完善,方便拓展
基础的编辑功能,比如拼接、排序、裁剪等等功能使用AVMutableComposition很容易就能实现。难么转场、滤镜、画中画这些怎么实现呢。使用AVMutableVideoComposition 可以指定视频的渲染尺寸、缩放比例、帧率等参数,转场动画可以通过添加AVVideoCompositionLayerInstruction指令实现。滤镜功能得针对每一帧视频来做处理,这里我使用AVVideoCompositing来处理。我们可以获取到每一帧的图片,在有多条轨道的时候,还能同时获取多个轨道的帧图片,我们可以在这做转场,滤镜,画中画这些功能。
2、框架设计
视频编辑主要分为以下几个模块
- 资源缓存(缓存当前视频编辑的资源,包括视频路径,图片、音乐、录音、视频帧图片)
- 编辑描述 (对视频编辑操作的描述,可以导出为JSON文件,也可以从JSON文件还原到编辑状态)
- 预览构建 (构建视频预览、处理编辑描述形成能够播放的视频资源)
- 视频预览 (播放预览构建的资源)
- 导出构建 (构建视频导出、处理编辑描述形成能够导出的视频资源)
- UI
下图是整体的一个流程
在视频编辑的时候,每次编辑都是对编辑描述模块的一个修改,后续所有的预览和导出都依赖于对编辑描述的解析。从视频编辑描述我们可以生成用来播放和导出的资源组合(AVMutableComposition),那么我们怎么来描述视频编辑呢。
首先我们看下视频编辑之后的资源组合是怎么样的:
- 时间线 :首先有个时间线的概念,我们所有的资源都是位于时间上的一段,每个资源都处于时间线上,所有操作也都是位于时间下上的一段时间。时间线的长度也就代表了最终视频合成的长度。
- 轨道:以时间线为坐标系的容器,容器内存放的是每个时间点需要的内容素材及编辑功能,可以分为视频轨道,音频轨道,画中画轨道、文字轨道
然后我们对上面图的编辑模型做描述
- 我们创建一个时间长度为100s的视频,拥有五条轨道,视频轨道包含转场描述、滤镜描述
- 视频轨道分两部分,一部分是基本视频,一部分是画中画视频
- 基础视频:包含5个视频段,分别位于两个视频轨道,类型都是video,所在位置分别为[0—30]、[42—55]、[75—100]、[25—45]、[60—78]
- 画中画:只包含一个视频段,类型是pip,所在位置是[8—92]
- 图片轨道:包含两个图片,类型是image,位置为:[28—45]、[62—90]
- 音频轨道:实例中只包含一条视频轨道,一个视频段,位置为[0—100],实际中可以跟视频一样有多条轨道,多个视频段
- 转场、滤镜描述:在这里也可以把转场滤镜当作完整的轨道,拥有类型和位置描述,跟视频段一样
有了上述的描述,我们可以将描述转换为相应的代码:
时间线:
我们创建一个时间线的描述类:FXTimelineDescribe
,它将拥有下面这些属性,用来存放各个轨道描述文件
CMTime duration; //时间线总时长
NSMutableArray<FXVideoDescribe *> *videoArray; //视频轨道视频段描述数组
NSMutableArray<FXPIPVideoDescribe *> *pipVideoArray; //画中画
NSMutableArray<FXAudioDescribe *> *audioArray; //音频(视频资源中自带的音频)
NSMutableArray<FXTransitionDescribe *> *transitionArray; //转场描述
NSMutableArray<FXMusicDescribe *>*musicArray; //音乐描述
NSMutableArray *filterArray; //滤镜描述
NSMutableArray *titleArray; //字幕描述
NSMutableArray *overlayArray; //水印描述
轨道描述:
根据类型不同,我们创建不同类型的轨道描述:
typedef NS_ENUM(NSUInteger, FXDescribeType) {
FXDescribeTypeNone,
FXDescribeTypeVideo, //视频
FXDescribeTypeAudio, //音频
FXDescribeTypeTransition, //转场
FXDescribeTypeTitle, //字幕
FXDescribeTypePip, //画中画
FXDescribeTypeMusic, //音乐
FXDescribeTypeRecord //配音
};
然后抽出轨道描述相同的部分作为轨道内容描述的基类:FXDescribe
@interface FXDescribe : NSObject
@property (nonatomic, assign) CMTime startTime; //开始时间
@property (nonatomic, assign) CMTime duration; //持续时间
@property (nonatomic, assign) CMTimeRange sourceRange; //在源资源中的位置
@property (nonatomic, assign) CGFloat scale; //变速的倍速
@property (nonatomic, assign) FXDescribeType desType; //类型
- (NSDictionary *)objectDictionary;
- (void)setObjectWithDic:(NSDictionary *)dic;
@end
其中添加了一个sourceRange属性,用来描述当前资源在源资源中的位置,比如视频段截取自某一个视频的一部分。
其中有两个方法需要子类重写的
1、(NSDictionary )objectDictionary; 2、(void)setObjectWithDic:(NSDictionary )dic;
1、(NSDictionary *)objectDictionary;
用来将描述转换为字典,后续组合起来转换为描述JSON,子类需要重写这个方法,然后将子类新增的属性也添加进来
2、(void)setObjectWithDic:(NSDictionary *)dic
这部分相当于数模转换,在这里没有使用自动数模转化(比如用YYModel),因为要处理一些比较特殊的数据,还有部分资源的查找。
接下来我们看下视频轨道的视频段是怎么描述的:
我们创建FXVideoDescribe,继承自:FXDescribe,我们需要添加视频特有的一些描述属性
@property (nonatomic, assign) NSInteger videoIndex; //视频段编号,后续用来做视频排序
@property (nonatomic, assign) BOOL reverse; //视频反转
@property (nonatomic, readonly) FXRotation rotate; //视频旋转,支持90、180、270度的旋转
@property (nonatomic, strong) FXVideoItem *videoItem; //视频资源,包含真实的视频音频轨道,帧缩略图资源等
@property (nonatomic, assign) BOOL mute; //是否静音
跟视频段描述相似,其他类型的描述添加各自需要的属性。
最终把时间线转换为字典:(我们添加两段视频,添加一个转场特效,然后将第一个视频分割为两段)我们来看下生成的描述字典
{
audioTrack = (
);
defaultNaturalSizeHeight = 1080;
defaultNaturalSizeWidth = 608;
duration = "30.182";
lengthTimeScale = 30;
mainVideoVolume = 100;
pipVideoVolume = 100;
transitionTrack = (
{
backVideoIndex = 1;
desType = 3;
duration = 2;
preVideoIndex = 0;
scale = 1;
sourceRangeDuration = nan;
sourceRangeStart = nan;
startTime = 0;
transType = 1;
}
);
videoTrack = (
{
desType = 1;
duration = "5.471666666666667";
filterType = 2;
mute = 0;
reverse = 0;
rotate = 0;
scale = 1;
sourceRangeDuration = "5.471666666666667";
sourceRangeStart = 0;
startTime = 0;
videoIndex = 0;
videoItem = "file:///var/mobile/Media/PhotoData/CPLAssets/group115/480C781E-4B41-48A3-B367-484F5C693464.MP4";
},
{
desType = 1;
duration = "8.389333333333333";
filterType = 2;
mute = 0;
reverse = 0;
rotate = 0;
scale = 1;
sourceRangeDuration = "8.389333333333333";
sourceRangeStart = "5.471666666666667";
startTime = "3.471666666666667";
videoIndex = 1;
videoItem = "file:///var/mobile/Media/PhotoData/CPLAssets/group115/480C781E-4B41-48A3-B367-484F5C693464.MP4";
},
{
desType = 1;
duration = "18.321";
filterType = 2;
mute = 0;
reverse = 0;
rotate = 0;
scale = 1;
sourceRangeDuration = "18.321";
sourceRangeStart = 0;
startTime = "11.861";
videoIndex = 2;
videoItem = "file:///var/mobile/Media/PhotoData/CPLAssets/group259/98D7CEA4-69EA-4EB8-924C-FB99DCDBBDD7.MP4";
}
);
}
转换为JSON 字符串:
{"pipVideoVolume":"100","mainVideoVolume":"100","transitionTrack":[{"scale":1,"desType":3,"backVideoIndex":"1","sourceRangeDuration":"nan","duration":"2","preVideoIndex":"0","startTime":"0","sourceRangeStart":"nan","transType":"1"}],"defaultNaturalSizeWidth":"608","duration":"30.182","audioTrack":[],"lengthTimeScale":"30","videoTrack":[{"scale":1,"reverse":"0","rotate":"0","desType":1,"sourceRangeStart":"0","sourceRangeDuration":"5.471666666666667","filterType":"2","videoIndex":"0","videoItem":"file:\/\/\/var\/mobile\/Media\/PhotoData\/CPLAssets\/group115\/480C781E-4B41-48A3-B367-484F5C693464.MP4","duration":"5.471666666666667","mute":"0","startTime":"0"},{"scale":1,"reverse":"0","rotate":"0","desType":1,"sourceRangeStart":"5.471666666666667","sourceRangeDuration":"8.389333333333333","filterType":"2","videoIndex":"1","videoItem":"file:\/\/\/var\/mobile\/Media\/PhotoData\/CPLAssets\/group115\/480C781E-4B41-48A3-B367-484F5C693464.MP4","duration":"8.389333333333333","mute":"0","startTime":"3.471666666666667"},{"scale":1,"reverse":"0","rotate":"0","desType":1,"sourceRangeStart":"0","sourceRangeDuration":"18.321","filterType":"2","videoIndex":"2","videoItem":"file:\/\/\/var\/mobile\/Media\/PhotoData\/CPLAssets\/group259\/98D7CEA4-69EA-4EB8-924C-FB99DCDBBDD7.MP4","duration":"18.321","mute":"0","startTime":"11.861"}],"defaultNaturalSizeHeight":"1080"}
至此我们的视频编辑描述部分就完成了,我们可以根据描述文件反推会相应的描述模型。因此,我们可以将每次修改之后的描述JSON文件存储起来,作为修改的一个状态,用来做撤销和重做操作
(也可以使用NSUndoManager来实现撤销重做,但是相对来说比较复杂一点)。
有了编辑描述之后,我们就可以根据编辑描述来构建UI,构建播放、导出模块。最终的结构如下图