姬長信(Redy)

马蜂窝视频编辑框架设计及在 iOS 端的业务实践

**_(马蜂窝技术公众号原创内容/uff0cID: mfwtech)_** 熟悉马蜂窝的朋友一定知道/uff0c点击马蜂窝 App 首页的发布按钮/uff0c会发现发布的内容已经被简化成「图文」或者「视频」。 长期以来/uff0c游记、问答、攻略等图文形式的形态一直是马蜂窝发展的优势所在。将短视频提升至与图文并列的位置/uff0c是因为对于今天的移动互联网用户来说/uff0c内容更真实直观、信息密度更大、沉浸感更强的短视频已经成为刚需。为了使旅游用户拥有更好的内容交互体验/uff0c丰富和完整原有的内容生态体系/uff0c马蜂窝加码了对短视频领域的布局。 现在/uff0c每天都会有大量短视频在马蜂窝产生/uff0c覆盖美食、探店、景点打卡、住宿体验等多种当地玩乐场景。马蜂窝希望平台的短视频内容除了「好看」之外/uff0c更要「好用」。这个「好用」不仅仅是为有需求的用户提供好用的旅行信息/uff0c更是指通过技术让用户的短视频创作更加简单易行。 为此/uff0c我们在马蜂窝旅游 App 的视频编辑功能中提供了「自定义编辑」与「模板创作」两种编辑模式/uff0c使用户既可以通过「模板」快速创作与模板视频同款的炫酷视频/uff0c也能够进入「自定义编辑」模式发挥自己的创意/uff0c生成个性化视频。 本文将围绕马蜂窝旅游 App iOS 端中的视频编辑功能/uff0c和大家分享我们团队视频编辑框架的设计及业务实践。 # Part.1 需求分析 如前言所述/uff0c我们要做的是能够支持「自定义编辑」与「模板创作」两种模式的视频编辑功能。 ![](https://oscimg.oschina.net/oscnet/8948760dc5d186187434d74187bf176554c.jpg) **图1/uff1a产品示意图** 首先我们梳理一下「自定义编辑」模式下/uff0c需要提供的功能/uff1a - 视频拼接/uff1a将多段视频按顺序拼接成一段视频 - 播放图片/uff1a将多张图片合成一段视频 - 视频裁剪/uff1a删除视频中某个时间段的内容 - 视频变速/uff1a调整视频的播放速度 - 背景音乐/uff1a添加背景音乐/uff0c可以与视频原音做混音 - 视频倒播/uff1a视频倒序播放 - 转场过渡/uff1a拼接的两段视频切换时增加一些过渡效果 - 画面编辑/uff1a画面旋转/uff0c画布分区、设置背景色/uff0c增加滤镜、贴纸、文字等附加信息 有了上述这些功能/uff0c便可以满足「自定义编辑」模式的需求/uff0c能够让用户通过我们的视频编辑功能完成自己的创作。但是为了进一步降低视频编辑功能的使用门槛/uff0c让制作炫酷视频变得简单/uff0c我们还需要支持「模板创作」模式。即为用户提供「模板视频」/uff0c用户只需要选择视频或者图片/uff0c便可创作出与「模板视频」有同样编辑特效的同款视频/uff0c实现「一键编辑」。 支持「模板创作」模式后/uff0c我们视频编辑功能最终的流程图如下/uff1a ![](https://oscimg.oschina.net/oscnet/d7138a2173c2cfa3b82b86ec225d647fc43.jpg) **图2/uff1a完整流程图** 如图所示/uff0c在媒体文件之外/uff0c多了一个模板 A 的输入/uff0c模板 A 描述了要对用户选择的媒体文件做哪些编辑。同时在编辑器的输出中多了一个模板 B/uff0c模板 B 描述了用户在完成编辑后/uff0c最终做了哪些编辑。其中模板 B 的输出/uff0c为我们解决了「模板视频」来源的问题/uff0c即「模板视频」既可以通过运营手段生产/uff0c也可以将用户通过「自定义编辑」模式创作的视频作为模板视频/uff0c使其他用户浏览该用户发布的视频时/uff0c可以快速创作同款视频。 通过上述需求分析的过程/uff0c可以总结出我们的视频编辑功能主要支持两个能力/uff1a 1. **常规视频编辑的能力** 2. **描述如何编辑的能力** 这两个能力的划分/uff0c为我们接下来进行视频编辑框架的设计提供了方向。 # Part.2 框架设计 常规视频编辑的能力是一个视频编辑框架需要提供的基本能力/uff0c能够支撑业务上的「自定义编辑」模式。「描述如何编辑」的能力则是将常规视频编辑能力进行抽象建模/uff0c描述「对视频做哪些编辑」这件事/uff0c然后将这种描述模型转化为具体的视频编辑功能/uff0c便能够支撑起业务上的「模板创作」模式。所以我们的编辑框架可以划分为两个主要的模块/uff1a - 编辑模块 - 描述模块 在两个模块之间/uff0c还需要一个转换模块/uff0c完成视频编辑模块与描述模块之间的双向转换。下图为我们需要的视频编辑框架示意图/uff1a ![在此输入链接描述][1] **图3/uff1a视频编辑框架示意图** - **编辑模块**所需要的具体功能/uff0c可以随着业务上的需求不断迭代添加/uff0c目前我们要支持的功能如图中所列。 - **描述模块**则需要一个描述模型/uff0c将媒体素材与各种编辑功能完整的描述出来。同时也需要将模型保存成文件/uff0c从而能够被传输分发/uff0c我们称之为描述文件。 - 另外在描述文件的基础上/uff0c「模板创作」模式中的「模板」还需要标题、封面图等运营相关的信息。所以还需要提供一个**运营加工**的功能/uff0c能够让运营同事将描述文件加工为模板。 - **转换模块**负责的则是将视频编辑功能抽象为描述文件、将描述文件解析为具体的编辑功能的任务/uff0c保证抽象与解析的正确性至关重要。 视频编辑模块在不同的开发平台上都有很好的实现方案/uff0c比如 iOS 原生提供的 AVFoundation/uff0c使用广泛的第三方开源库 GPUImage/uff0c以及更加通用的 ffmpeg 等。具体的实现方案可以结合业务场景与项目规划进行选择/uff0c我们目前在 iOS 端采用的方案是苹果原生的 AVFoundation。如何结合 AVFoundation 实现我们的视频编辑框架会在下文具体介绍。接下来我们就来看下具体功能模块的设计与实现。 # Part.3 模块功能与实现 ## 3.1 描述模块 ### **3.1.1 功能划分** 首先我们对「自定义编辑」模式下需要支持的具体功能进行分析/uff0c发现可以以编辑的对象为标准/uff0c将编辑功能划分为两类/uff1a段落编辑、画面编辑。 - **段落编辑**/uff1a将视频段看作编辑对象/uff0c不关心画面内容/uff0c只在视频段层面上进行编辑/uff0c包含如下功能/uff1a ![](https://oscimg.oschina.net/oscnet/c46ffd02e5cadb0fba936c847c30c24722c.jpg) **图4/uff1a段落编辑** - **画面编辑**/uff1a将画面内容作为编辑对象/uff0c包含如下功能/uff1a ![在此输入链接描述][2] **图5/uff1a画面编辑** ### **3.1.2 视频编辑描述模型** 有了编辑功能的划分后/uff0c要描述「对视频进行哪些编辑」/uff0c我们还需要一个视频编辑描述模型。定义如下几个概念/uff1a - **时间线**/uff1a由时间点组成的单向递增直线/uff0c起始点为 0 点 - **轨道**/uff1a以时间线为坐标系的容器/uff0c容器内存放的是每个时间点需要的内容素材及「画面编辑」功能 1. 轨道具有类型/uff0c一条轨道仅支持一种类型 - **段落**/uff1a轨道中的一段/uff0c即轨道所属时间线上两个时间点及两点之间的部分 1. 段落也具有类型/uff0c与其所属轨道类型保持一致 轨道类型列表/uff1a ![](https://oscimg.oschina.net/oscnet/b0ffbc824516710c8ed3cee9d0210cfe97a.jpg) 其中「视频」、「图片」、「音频」类型轨道/uff0c是提供画面与声音内容的轨道。其余几个类型的轨道/uff0c则是用于描述具体做哪些画面编辑功能的轨道。特效类型轨道中可以指定若干画面编辑效果/uff0c如旋转、分区等。 结合编辑功能的划分/uff0c我们可以看出段落编辑功能的编辑对象是轨道内的段落/uff0c画面编辑功能的编辑对象是轨道内存放的内容素材。 有了时间线、轨道、段落三个概念以及段落编辑、画面编辑两个编辑行为的划分后/uff0c我们在抽象层面描述视频的编辑过程如下/uff1a ![在此输入链接描述][3] **图6/uff1a视频编辑描述模型示意图** 如上图所示/uff0c通过该模型/uff0c我们已经能够完整的描述出「对视频进行哪些编辑」/uff1a - 创作一个时长 60 秒的视频/uff0c内容素材有视频/uff0c图片/uff0c音乐/uff0c分别对应轨道 1、轨道 2、轨道 3/uff0c并且有转场、滤镜效果/uff0c由轨道 4、轨道 5 指定/uff08其他效果不再单独描述/uff0c以转场、滤镜效果为参考/uff09。 - 该视频由轨道 1 的 /[0-20/] 段、轨道 2 的 /[15-35/] 段、轨道 1 的 /[30-50/] 段以及轨道 2 的 /[45-60/] 段拼接而成。 - /[0-60/] 视频全段有背景音乐/uff0c音乐由轨道 3 指定。 - /[15-20/]、/[30-35/]、/[45-50/] 三段内有转场效果/uff0c转场效果由轨道 4 指定。 - /[15-35/] 段有滤镜效果/uff0c滤镜效果由轨道 5 指定。 ### **3.1.3 描述文件与模板** 有了上述的视频编辑描述模型后/uff0c我们还需要具体的文件来存储和分发该模型/uff0c即描述文件/uff0c我们使用 JSON 文件来实现。同时还需要提供运营加工的能力/uff0c使运营同事为描述文件添加一些运营信息/uff0c生成模板。 - **描述文件**: 根据视频编辑模型生成一份 JSON 文件 举个/ud83c/udf30 ``` {     "tracks": [{             "type": "video",             "name": "track_1",             "duration": 20,             "segments": [{                 "position": 0,                 "duration": 20             }, ...]         }, {             "type": "photo",             "name": "track_2",             "duration": 20,             "segments": [{                 "position": 15,                 "duration": 20             }, ...]         },         {             "type": "audio",             "name": "track_3",             "duration": 60,             "segments": [{                 "position": 0,                 "duration": 60             }]         }, {             "type": "transition",             "name": "track_4",             "duration": 5,             "segments": [{                 "subtype": "fade_black",                 "position": 15,                 "duration": 5             }, ...]         }, {             "type": "filter",             "name": "track_5",             "duration": 20,             "segments": [{                 "position": 15,                 "duration": 20             }]         }, ...     ] } ``` - 模板: 由描述文件+若干业务信息组成的JSON文件 举个/ud83c/udf30 ``` {     "title": "模板标题",     "thumbnail": "封面地址",     "description": "模板简介",     "profile": {    //描述文件         "tracks": [...]        } } ``` 通过上述视频编辑描述模型与描述文件及模板的定义/uff0c结合转换器/uff0c我们便可以在用户使用「自定义」编辑功能的基础上/uff0c生成一份描述文件来描述用户最终对视频进行的编辑行为。反过来我们也可以通过解析描述文件/uff0c将用户选择的素材根据描述文件进行编辑/uff0c快速生成与描述文件中的编辑行为「同款」的视频。 ## 3.2 编辑模块 ### **3.2.1 AVFoundation 介绍** AVFoundation 音视频编辑分为素材混合、音频处理、视频处理、导出视频这四个流程。 **/uff081/uff09素材混合器 AVMutableComposition** AVMutableComposition 是一个或多个轨道/uff08AVCompositionTrack/uff09的集合/uff0c每个轨道会根据时间线存储源媒体的文件信息/uff0c比如音频、视频等。 ``` //AVMutableComposition 创建一个新AVCompositionTrack的API - (nullable AVMutableCompositionTrack *)addMutableTrackWithMediaType:(AVMediaType)mediaType preferredTrackID:(CMPersistentTrackID)preferredTrackID; ``` 每个轨道由一系列轨道段/uff08track segments/uff09组成/uff0c每个轨道段存储源文件的一部分媒体数据/uff0c比如 URL、轨道 ID/uff08track identifier/uff09、时间映射/uff08time mapping/uff09等。 ``` //AVMutableCompositionTrack部分属性 /* provides a reference to the AVAsset of which the AVAssetTrack is a part */ AVAsset *asset; /* indicates the persistent unique identifier for this track of the asset */ CMPersistentTrackID trackID; NSArray *segments; ``` 其中 URL 指定文件的源容器/uff0c轨道 ID 指定需要用到的源文件轨道/uff0c而时间映射指定的是源轨道的时间范围/uff0c并且还指定其在合成轨道上的时间范围。 ``` //AVCompositionTrackSegment的时间映射 CMTimeMapping timeMapping; //CMTimeMapping定义 typedef struct { CMTimeRange source; // eg, media. source.start is kCMTimeInvalid for empty edits. CMTimeRange target; // eg, track. } CMTimeMapping; ``` ![](https://oscimg.oschina.net/oscnet/28a12e46e8f26d5c8006e4360c2b1e24344.jpg) **图7/uff1aAVMutableComposition合成新视频的流程 ** _**/uff08来源/uff1a苹果官方开发者文档/uff09**_ **(2) 音频混合 AVMutableAudioMix ** AVMutableAudioMix 可以通过 AVMutableAudioMixInputParameters 指定任意轨道的任意时间段音量。 ``` //AVMutableAudioMixInputParameters相关api CMPersistentTrackID trackID; - (void)setVolumeRampFromStartVolume:(float)startVolume toEndVolume:(float)endVolume timeRange:(CMTimeRange)timeRange; ``` ![](https://oscimg.oschina.net/oscnet/61caa0d9aed0f452550fc77b5801ec6f2d9.jpg) **图8/uff1a音频混合示意图** _**/uff08来源/uff1a苹果官方开发者文档/uff09**_ **/uff083/uff09视频渲染 AVMutableVideoComposition** 我们还可以使用 AVMutableVideoComposition 来直接处理 composition 中的视频轨道。处理一个单独的 video composition 时/uff0c你可以指定它的渲染尺寸、缩放比例、帧率等参数并输出最终的视频文件。通过一些针对 video composition 的指令/uff08AVMutableVideoCompositionInstruction 等/uff09/uff0c我们可以修改视频的背景颜色、应用 layer instructions。 这些 layer instructions/uff08AVMutableVideoCompositionLayerInstruction 等/uff09可以用来对 composition 中的视频轨道实施图形变换、添加图形渐变、透明度变换、增加透明度渐变。此外/uff0c你还能通过设置 video composition 的 animationTool 属性来应用 Core Animation Framework 框架中的动画效果。 ![](https://oscimg.oschina.net/oscnet/75fd50a588e2596ffa287b36cf3f729b1a2.jpg) **图9/uff1aAVMutableVideoComposition 处理视频 ** _**/uff08来源/uff1a苹果官方开发者文档/uff09**_ **(4) 导出 AVAssetExportSession** 导出的步骤比较简单/uff0c只需要把上面几步创建的处理对象赋值给导出类对象就可以导出最终的产品。 ![](https://oscimg.oschina.net/oscnet/afa25f494023e0ebaf3b640e930acf84254.jpg) **图10/uff1a导出流程** _**/uff08来源/uff1a苹果官方开发者文档/uff09**_ ### **3.2.2 编辑模块的实现** 结合 AVFoundation 框架/uff0c我们在视频编辑模块中分别实现了如下几个角色/uff1a - **轨道**/uff1a有视频与音频两种类型/uff0c存放帧图与声音。 1. 在视频类型的轨道中/uff0c扩展出图片类型轨道/uff0c即通过空的视频文件生成视频轨道/uff0c将所选的图片作为帧图提供给混合器。 2. 附加轨道/uff1aAVFoundation 提供了 AVVideoCompositionCoreAnimationTool 这个工具/uff0c能够方便的将 CoreAnimation 框架内的内容应用到视频帧图上。所以添加文字的功能/uff0c我们在预览端通过 UIKit 创建了一系列预览视图/uff0c导出时转换到该工具的 CALayer 上。 - **段落**/uff1a轨道中的某个时间段/uff0c作为段落编辑的对象。 - **指令**/uff1a关联到指定的视频段落/uff0c进行图片处理/uff0c绘制每一帧画面。 1. 一个指令可以关联多条视频轨道/uff0c从这些视频轨道的指定时间段内获取帧图/uff0c作为画面编辑的对象。 2. 指令中画面编辑的具体实现方案/uff0c采用 CoreImage 框架。CoreImage 本身提供了一些内置的图片实时处理功能。CoreImage 不支持的特效/uff0c我们通过自定义 CIKernel 的方式来实现。 - **音频混音器**/uff1a针对添加音乐的功能/uff0c我们使用 AVMutableAudioMix 完成。 - **视频混合器**/uff1a我们最终要得到的视频文件/uff0c一般包含一条视频轨道、一条音频轨道。混合器就是将我们输入的媒体资源转换为轨道/uff0c根据用户的操作或者由描述模型转换/uff0c对视频进行段落编辑、组装指令进行画面编辑/uff0c对音频轨道进行混音/uff0c结合 AVPlayerItem 与 AVExportSession/uff0c提供实时预览与最终合成的功能。 有了上述几个角色后/uff0c在 iOS 端的视频编辑模块实现示意图如下/uff1a ![](https://oscimg.oschina.net/oscnet/929679a05a5202a0132e2b60f93fa85e504.jpg) **图11/uff1a视频编辑模块示意图** 如上图所示/uff0c混合器中包含两条视频轨道与一条音频轨道。一般来说输入的视频与图片文件/uff0c都会生成一条对应的视频轨道/uff0c理论上混合器中应该有多条视频轨道。我们图中混合器只保持两条视频轨道与一条音频轨道/uff0c首先是为解决视频解码器数量限制的问题/uff0c后面会有具体描述。其次是为了保证实现转场过渡功能。 指令序列由若干在时间段上连续的指令组成/uff0c每个指令由时间段、帧图来源轨道、画面编辑效果组成。视频编辑功能中的段落编辑功能/uff0c便是对指令段的拼接/uff1b画面编辑功能/uff0c便是每个指令段对帧图做的编辑处理。混合器提供的预览功能/uff0c可以将编辑改动实时展示给用户。确定了编辑效果后/uff0c通过混合器提供的合成功能/uff0c便完成了最终视频文件的合成。 ### 3.2.3 转换器  有了视频编辑模块的实现方案后/uff0c我们已经能够支持「自定义编辑」模式。最后通过转换器的连接/uff0c便可以将描述模型与编辑模块整合在一起/uff0c完成对「模板创作」模式的支持。这里转换器的实现较为简单/uff0c即将 JSON 格式描述文件解析为数据模型/uff0c混合器根据用户选择的素材与描述模型创建自己内部的轨道模型/uff0c拼接指令段即可。 另一方面/uff0c混合器在完成编辑导出时/uff0c将自己内部的轨道模型与指令信息组装成数据模型/uff0c生成 JSON 格式的描述文件。 ![](https://oscimg.oschina.net/oscnet/ca8b230ac273161dd49dcd578dd3b4831a3.jpg) **图12/uff1a描述模型与编辑模块相关转换** # Part.4 近期优化方向 ## 4.1 踩过的坑 在实现上述编辑框架的过程中/uff0c我们遇到过很多的问题/uff0c多数是由于 AVFoundation 中错误信息不够明确/uff0c定位问题比较耗时/uff0c总结起来大部分都是轨道时间轴对齐引起的问题。除时间轴对齐问题外/uff0c我们在这里总结几个在实现时需要考虑到的问题/uff0c与大家分享一下/uff0c避免踩同样的坑。 **(1) 混合器轨道数量限制** - **问题**/uff1aAVMutableComposition 可以同时添加很多的轨道/uff0c即一个 composition 内可以同时存在多条视频轨道/uff0c并且可以正常通过 AVPlayer 预览播放。所以我们最初实现的编辑模块/uff0c混合器中支持多条视频轨道/uff0c如下图所示。这种多条轨道结构/uff0c预览时没有问题/uff0c导出时却出现了「无法解码」的报错。转换前的混合器结构图/uff1a ![](https://oscimg.oschina.net/oscnet/3b3d7616caae8bb076e12bda6f14e9496b0.jpg) **图13/uff1a转换前的混合器结构图** - **原因**/uff1a经查证后发现是苹果设备对视频播放解码器的数量有限制/uff0c导出视频时/uff0c每条视频轨道会使用一个解码器/uff0c这就导致导出时如果视频轨道数量超出解码器数量的限制/uff0c无法导出。 - **解决方案**/uff1a轨道模型转换/uff0c将最初混合器中的多视轨结构转换为目前的双视轨结构/uff0c这样在导出时便不会超出解码器数量限制。 **(2) 性能优化/uff1a倒播功能实现方案** - **问题**/uff1a最初的实现方案为导出一个新的视频文件/uff0c帧序列是原视频文件中的倒序。如果原视频文件很大/uff0c用户只裁剪了一种的一段/uff0c对这一段进行倒播时/uff0c仍然会将原视频文件进行倒序处理/uff0c导出一份新的视频/uff0c是一个十分耗时的操作。 - **解决方案**/uff1a根据时间点获取视频文件的对应帧/uff0c倒播时只是将正常时间点转换为倒播后的时间点即可/uff0c不操作视频文件。类似于对数组的操作/uff0c只操作下标/uff0c而不直接改变数组的顺序。 **(3) 性能优化/uff1a减少内存使用率** - **问题**/uff1a预览时使用原图尺寸/uff0c消耗内存严重/uff0c添加多个高清图片后/uff0c预览过程会出现内存告警。 - **解决方案**/uff1a在不影响用户体验的情况下/uff0c使用低分辨率的图片进行预览/uff0c导出时使用原图。 ## 4.2 近期规划 目前这套视频编辑框架在马蜂窝旅游 App iOS 端运作良好/uff0c能够支撑业务的不断迭代/uff0c可以快速扩展出更多的画面编辑功能/uff0c当然也还有一些需要优化的细节有待完善。 近期我们会结合机器学习与 AR 技术/uff0c探索一些有趣好玩的视频编辑场景/uff0c为用户提供更加个性化的旅行记录工具。 **本文作者/uff1a李旭、赵成峰/uff0c马蜂窝内容中心 iOS 研发工程师。** ![](https://mmbiz.qpic.cn/mmbiz_png/69F4CS2PEMtWHMMtckVqUH3xZIhiac5v5y7ldl5Ea19Qia1WPAibpbvh0tk4BvHuWZsvttw9gAj5E2DjRWSyDVkgQ/640?wx_fmt=png) [1]: http://api.cocoachina.com/uploads/20190903/201909030951534590.jpg [2]: http://api.cocoachina.com/uploads/20190903/201909030952086474.jpg [3]: http://api.cocoachina.com/uploads/20190903/201909030952221892.jpg