iOS视频编辑从设计到实现-(3)框架设计

2,430 阅读8分钟

最初下载了几个比较好用的编辑软件体验,大都功能比较完善,体验下来发现分为两大模块

  • 自定义编辑模式
  • 模板编辑模式

自定义编辑模式,这里面包含的功能比较多,大部分的实现都能找到单独的例子,难点在于怎么把所有的这些功能组合起来,如何编辑,如何预览。

  • 视频拼接
  • 视频裁剪
  • 视频排序
  • 视频分割
  • 视频旋转
  • 视频变速
  • 删除视频
  • 调整画幅
  • 调整音量
  • 画中画
  • 滤镜
  • 视频转场
  • 视频倒播
  • 背景音乐
  • 配乐
  • 字幕
  • 保存草稿
  • 重做功能
  • 导出视频

模板编辑模式,即为用户提供「模板视频」,用户只需要选择视频或者图片,便可创作出与「模板视频」有同样编辑特效的同款视频,实现「一键编辑」。

1、技术选型

在iOS平台上,能够实现上面的自定义编辑功能的技术有不少。比如AVFoundation、GPUImage、FFMPEG,本文主要基于AVFoundation来实现。

主要考虑的几个点:

1、使用AVFoundation可以很方便预览,使用AVPlayer来做编辑预览

2、使用AVFoundation性能有保障,后续做优化比较容易,接口比较完善,方便拓展

基础的编辑功能,比如拼接、排序、裁剪等等功能使用AVMutableComposition很容易就能实现。难么转场、滤镜、画中画这些怎么实现呢。使用AVMutableVideoComposition 可以指定视频的渲染尺寸、缩放比例、帧率等参数,转场动画可以通过添加AVVideoCompositionLayerInstruction指令实现。滤镜功能得针对每一帧视频来做处理,这里我使用AVVideoCompositing来处理。我们可以获取到每一帧的图片,在有多条轨道的时候,还能同时获取多个轨道的帧图片,我们可以在这做转场,滤镜,画中画这些功能。

2、框架设计

视频编辑主要分为以下几个模块

  • 资源缓存(缓存当前视频编辑的资源,包括视频路径,图片、音乐、录音、视频帧图片)
  • 编辑描述 (对视频编辑操作的描述,可以导出为JSON文件,也可以从JSON文件还原到编辑状态)
  • 预览构建 (构建视频预览、处理编辑描述形成能够播放的视频资源)
  • 视频预览 (播放预览构建的资源)
  • 导出构建 (构建视频导出、处理编辑描述形成能够导出的视频资源)
  • UI

下图是整体的一个流程 编辑流程

在视频编辑的时候,每次编辑都是对编辑描述模块的一个修改,后续所有的预览和导出都依赖于对编辑描述的解析。从视频编辑描述我们可以生成用来播放和导出的资源组合(AVMutableComposition),那么我们怎么来描述视频编辑呢。

首先我们看下视频编辑之后的资源组合是怎么样的:

编辑轨道示意图

  • 时间线 :首先有个时间线的概念,我们所有的资源都是位于时间上的一段,每个资源都处于时间线上,所有操作也都是位于时间下上的一段时间。时间线的长度也就代表了最终视频合成的长度。 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
  • 轨道:以时间线为坐标系的容器,容器内存放的是每个时间点需要的内容素材及编辑功能,可以分为视频轨道,音频轨道,画中画轨道、文字轨道

然后我们对上面图的编辑模型做描述

  • 我们创建一个时间长度为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,子类需要重写这个方法,然后将子类新增的属性也添加进来
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

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,构建播放、导出模块。最终的结构如下图

视频编辑流程1

访问我的 Github 仓库 查看更多精彩