淘系的音视频编辑方案:非线性编辑引擎
大貘2021-07-08


非编定义


非编是非线性编辑的简称,如果按照维基百科的解释,线性编辑和非线性编辑是相对的,是影视制作中的两种不同方案。

它们两者分别是录像带和数码两个时代不同的产物,最主要的区别在于是否是对原始数据进行破坏性编辑。

如果按照此解释,我们这里就应该把文章名改为《淘系的音视频多轨道编辑引擎设计》,才能更准确的说明我们想介绍的是什么。

所以我们更乐意的是抛开历史背景,从字面上来解释:无论音轨还是视轨,在时间线上无需依次线性排列的编辑方式,就是我们所说的非线性编辑

如下图示例,使用非线性编辑的方式,我们可以灵活自由的根据我们的需要,编排我们的音频和图像资源。可以按时间顺序依次编排,也可以在时间上重叠在一起,也可以相互交错。


随着移动设备的更新换代,网络速度的持续提升和费用降低,手机用户已经经历了从文字阅读到图片浏览再到视频观看的内容消费的变革。现如今大家在利用手机进行休闲、购物、学习、生活记录等许多方面,都更乐于以视频作为媒介了。而非线性编辑作为视频生产中一个重要的技术环节,在市场需求的推动下,也是发展的如火如荼。那么,淘系的音视频非线性编辑引擎Marvel是怎样设计的呢?

业务背景

近些年用户习惯逐渐从图文向短视频迁移,淘系大量的商家和用户,对于短视频内容的生产和消费诉求也日益提高。淘系生态下有着诸多内容生产的入口,面向不同的用户群体,不同的用户群体期望生产的内容也存在差异,如商家、达人和买家三者的生产目的可定就存在巨大差异。Marvel引擎就是期望为淘系生态下的所有入口,提供相对统一的解决办法,满足不同用户群体对于短视频的差异化的生产需求。

对于短视频内容生产,编辑工具通常会分化为两种,一种相对来说,功能不是那么强大,只具备一些简单的能力,使用起来非常简单,这一类我们称之为UGC工具。UGC追求简单,降低生产门槛。另外一种,功能非常齐全,可以编辑生产出特别复杂的内容,有一定的使用门槛,我们称之为PGC工具。PGC追求齐全,满足专业需求。淘系的短视频内容生产,显然是这两种工具都需要。

淘宝逛逛中的发布器就是淘系短视频UGC工具的代表,而淘宝为淘宝商家打造的亲拍,就是淘系短视频PGC工具的代表。这两个业务底层的音视频编辑能力,都是有Marvel引擎提供的。

目标挑战

淘系短视频内容生产的工具,需要围绕淘宝电商生态之下,包括商家、设计师、ISV、达人、买家等等诸多角色的用户而展开。为满足各维度用户的生产工具诉求,保证各维度用户的相互促进,我们需要打造一个体系化的跨平台音视频非线性编辑引擎。满足UGC、PGC、OGC等多维度的短视频内容生产工具的业务诉求。并以资源为媒介,在工具间进行流转,满足各维度的用户生产需求。

需要打造这样一个体系化的音视频非线性编辑引擎,满足淘系不同目标的短视频内容生产工具的发展需求,我们也面许多挑战:

▐ 场景覆盖


淘宝用户众多,包括商家、设计师、ISV、达人、买家等等诸多角色,各角色对于生产工具的诉求存在巨大差异,需要引擎同时满足不同用户、不同场景的生产诉求。

▐ 平台覆盖

从用户生产数据来看,Android和iOS等移动端是重要的内容生产入口,但是也存在大量精品内容,来自PC制作。引擎不仅要支持移动端,也要支持PC端的生产工具建设。

▐ 功能体积


我们需要保证足够完备丰富的功能,支持PGC用户的设计创意。同时,我们又需要严格控制引擎大小,尤其是支持手淘UGC发布器时,避免带来应用体积增量过大的负担。

▐ 稳定安全

作为短视频内容生产体系的技术基石,面向众多的生产用户,在保证多业务多平台快速发展的同时,我们也要保证引擎稳定安全。

技术选型


淘系的短视频内容生产工具的需求,不仅仅在移动端工具。整个淘系的短视频内容生产中,PC端、云端等配套工具也是不可或缺,有大量精品内容是来自PC制作。

所以我们的引擎构建目标,不仅要支持移动端生产工具的构架,也需要兼顾PC端、云端等生产场景。

业界许多短视频内容生产的解决方案,会选择复用AE生态,来解决设计工具与资源的问题。

一种是用AE来生产视频模板,然后通过SDK解析模板,生产视频。

还有一种会利用AE来生产特效,通过SDK来解析渲染特效,应用到编辑场景中。在当前阶段,我们对于复用AE生态的诉求并不强烈,当然,后续也不排除会考虑兼容AE生产的资源。


另外,对于引擎的实现,有些解决方案会选择基于开源音视频框架进行实现,例如GStreamer。GStreamer框架非常强大和通用,其内核小、模块化设计、功能强大、支持所有主流平台,但是也存在一些问题。

  1. 它的模块化和强大的功能,是以复杂的设计为代价的,这导致它上手较难,二次开发也不方便。
  2. 插件+流水线架构设计,在涉及时序功能、效率优化的方面,会存在链路过长、实现复杂的问题。
  1. GStreamer内核小、可裁剪,但是随着功能的增多,也会因其设计,导致引擎的大小不容易优化。
  2. 它是基于GObject+Glib进行面向对象的程序设计,对于我们这些Java/C++开发人员来很不友好。

所以,最终我们还是选择用C++复用淘系已有的渲染、音频处理等框架能力自己撸一个跨平台音视频非线性编辑引擎。

架构选型


我们可以粗略的将音视频的非线性编辑引擎的工作流程拆解为四个部分,依次为理解用户输入的编辑意图,把用户输入的编辑意图组织为引擎更容易理解的信息结构,然后将信息结构的内容按照可视听的方式反馈给用户,最后用户在需要的时候将编辑的产物进行导出。无论接入的业务如何变化,引擎内的流程都可以归到这四个步骤之内。


据此理解,为保证多种平台、不同场景下的业务诉求,我们对音视频生产处理引擎需要满足业务逻辑进行功能模块划分,构造满足通用多轨道非线性音视频编辑的数据结构,然后以此为核心,向下层设计提出要求,并在迭代过程中进行调整和补充,最终逐渐形成了如下图所示,自下而上包括基础层、驱动层、逻辑层和封装层四层结构的整体架构。

在封装层中,我们对接口分级,C++ Engine API提供相对通用的接口,平台层对其进行包装,然后业务层基于平台层的封装进行业务接口封装。使核心接口稳定,避免功能、场景、业务增加带来的接口膨胀。

在逻辑层中,我们按照非线性编辑引擎的工作流程,以综合的业务逻辑划分功能模块,剥离与具体业务场景的直接联系,让不同业务可以根据场景需要进行组合,来满足不同需求。

在驱动层中,我们构建了时序&数据关系、音画的驱动设备以及编辑上下文需要的多个子系统,用来管理和组织资源、数据源、材质、音画处理器等,依用户意图,结合基础层能力,按时序对外输出可视听的数据。

在基础层中,我们抽象了资源、音画源的接口,并针对不同类型做了实现,也提供了与业务逻辑无关的,相对底层的对象结构和基础能力。


数据结构


Marvel以“面板”、“轨道”、“片段”的概念来组合和处理多轨道非线性编辑的复杂时序逻辑,它们是Marvel数据结构概念的核心,以“材质”的概念,来区分、组合、满足对于非线性编辑中复杂的空间及功能逻辑,以“资源”的概念来保证和简化以Marvel为核心的短视频内容生产体系中资源的流转。

所有的资源的时序信息都通过面板(ClipPanel)来进行整合,一个编辑工程中有且仅有一个编辑面板,面板中可以存在一个或者多个轨道,每个轨道中可以存在一个或者多个片段。如下图所示。


抛开时序相关的功能和表现,不同类型的片段,对外的表现存在很大的差异,而且这些差异通常也要求外部可控可编辑,但是不同类型的片段,也会存在一些表现共同的地方。

比如一个视频片段显示到屏幕上,可以对这个视频图像进行缩放、位移、旋转、修改透明度、调整混合方式等,贴纸和文字等片段也需要能够进行这些操作,而这些操作对于声音片段、滤镜片段来说没有任何意义,也可以说无法应用。材质概念的设计就是为了处理这样的情况。

材质在渲染系统中常用的术语,表示的是表面各可视属性的结合。

Marvel借用了此术语,将其衍生为片段的所有附加属性(附加是相对片段定义已经存在的属性而言的)。

为了降低API上的理解成本,材质这个概念,会以片段的属性对外表现。

对于不同类型的片段,我们可以给他们增加不同的材质,在进行片段的处理时,我们根据片段的材质,找到对应材质的处理器进行处理,这样即对不同片段做了区分,也保证了相同处理流程的复用。

在Marvel中,当前材质可以按照其目标对象分为两类,一类是图像相关的,比如滤镜、特效、画面位移旋转缩放控制、画面效果调节等等。一类是音频相关的,比如变声效果、音量调整、降噪处理等等。Marvel中除时序功能外,其他功能基本都是通过材质来体现。

功能模块


作为通用的跨平台音视频非线性编辑引擎,以市场上Top10的视频编辑工具为假想业务,从大的功能上进行划分,Marvel需要满足音视频多轨道非线性的编辑(包括贴纸文字特效等等)、编辑过程的实时预览、编辑产物的导出(视频、草稿、模版等)、为满足二次编辑能力的编辑产物加载。另外为了降低业务接入的门槛,Marvel可能还需要支持常见的音视频处理操作,比如音视频分离、视频截帧等等。Marvel的逻辑层模块划分就是由此而来。

逻辑层各个模块都是直接或者间接的围绕ClipPanel中的数据来按照各自的职责展开各自的逻辑。其中,我们可以理解为,Editor模块关注的是ClipPanel原始的数据,Player模块更关注的是按照ClipPanel原始数据构建出的源所提供的数据流(主要有音频流和图像流)。Exporter和Toolbox则按需决定其关注点,比如导出草稿的Exporter关注的是ClipPanel的原始数据,而导出视频的Exporter关注的ClipPanel构建的源提供的数据流。我们以数据结构和逻辑模块来对下一个层次提出设计要求,也就是驱动层。

这里的驱动层的并不是操作系统的驱动层那样驱动硬件运行,但是它也具有硬件驱动类似的含义。硬件驱动是按照硬件的时序图操作硬件,使硬件进行工作,而Marvel的驱动层是针对ClipPanel原始数据和运行时的数据流而言,是按照ClipPanel所描述的时序,以生产者-消费者的模型来展开设计,驱动数据流动,实现音视频的编辑、预览及导出。

基础层的设计并不直接与逻辑层联系,是音视频生成处理中必不可却的部分。像音视频解码、图像解码、图像渲染、音频处理、文件读写等,这些基础的功能,无论上层如何设计,这些功能都必须提供,基础层的设计是剥离了逻辑层的需要进行思考和设计的,可能在后续的演进过程中,会因为逻辑层和业务的需要而有所改动,但是其原始的设计,我们认为是只与数据结构设计有关,和业务与逻辑层无关。


按照当前Marvel中的实现,内部数据流向大概如上图所示。Marvel中的ClipPane对Audio Device表现为一个提供音频原始数据的源,对Video Device表现为一个提供纹理数据的源。Audio Device和Video Device都是作为一个数据消费的中间装置而存在,按照时序获取数据,根据业务场景的不同,将数据传递给不同的消费者。

在处理流程中,音频源和纹理源提供出来的都是对于的复合节点,合成器用于解释并处理复合节点,将音频复合节点处理成PCM数据,将纹理复合节点处理成纹理数据。以音频为例,其处理流程大致如下,纹理的处理流程,也基本与此类似。


接口设计


与许多引擎或者SDK有所不同,由于视频编辑的复杂性,不同的业务场景对于视频编辑的诉求也有很大的差异,Marvel为了保证在维持接口简单清晰的基础上,满足不同业务的诉求,将接口进行了分层,这部分在架构图中也特意进行了区分和标注。Marvel Engine C++ API按照架构中逻辑层的划分进行类C接口定义,不区分业务场景进行接口设计。Platform API基本就是按照C++ API对各平台和语言进行的封装。

Marvel Engine C++ API的存在,主要是期望避免随着业务数量和业务诉求的增多,Marvel的API越来越臃肿最后难以维护,所以尽可能的屏蔽业务场景带来的影响,这样同样也使得这套接口对业务并不友好。所以在C++ API和 Platform API之上,还存在Business Layer API。Business Layer API是按照业务诉求对Platform API或者C++ API进行封装,我们担忧的接口膨胀,主要来源于Editor,Marvel会按照常见的视频编辑应用,内置一套通用的业务API,同时也允许业务接入方自行扩展。

C++接口和业务功能没有关联,把编辑动作简化为数据的增删改查,通过key-value来增删改查内部属性。业务层接口再对这个接口进行封装,可能是单一接口封装,肯能是多个接口组合,视业务需求而定,形成业务接口。如下,分别为C++接口和业务接口示例:

// 设置和获取片段的属性
int setClipStrProperty(EditorHandle h, const String& id, const String& type, const String& key, const String& value);
int setClipI64Property(EditorHandle h, const String& id, const String& type, const String& kye, int64_t value);
int setClipDblProperty(EditorHandle h, const String& id, const String& type, const String& kye, double value);
int getClipI64Property(EditorHandle h, const String& clipId, const String& type, const String& key, int64_t& value);
int getClipDblProperty(EditorHandle h, const String& clipId, const String& type, const String& key, double& value);
int getClipStrProperty(EditorHandle h, const String& clipId, const String& type, const String& key, String& value);
// 移除片段的属性
int removeClipProperty(EditorHandle h, const String& clipId, const String& type, const String& key);
// 设置资源的属性
int setResStrProperty(EditorHandle h, const String& resId, const String& key, const String& value);
int setResI64Property(EditorHandle h, const String& resId, const String& key, int64_t value);
int setResDblProperty(EditorHandle h, const String& resId, const String& key, double value);
int getResStrProperty(EditorHandle h, const String& resId, const String& key, String& value);
int getResI64Property(EditorHandle h, const String& resId, const String& key, int64_t& value);
int getResDblProperty(EditorHandle h, const String& resId, const String& key, double& value);


/**
* 设置一个片段上图像的裁剪信息,裁剪出来的区域为保留区域
*
* @param id 目标片段ID
* @param x x方向起始坐标
* @param y y方向起始坐标
* @param w 裁剪宽度
* @param h 裁剪高度
* @param rotate 裁剪旋转信息
* @param normalize 裁剪参数是否为归一化参数
* @param rotateWithCropCenter 旋转是否使用裁剪区域的中心作为旋转中,false时使用图片中心作为旋转中心
* @return 执行结果
*/
public int setClipCrop(String id, float x, float y, float w, float h, float rotate, boolean normalize, boolean rotateWithCropCenter) {
    int actRet = editor.cCheckToAddMtl(id, C.kMaterialTypeCrop);
    if (actRet != 0) {
        return actRet;
    }
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyRotate, rotate);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyXOffset, x);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyYOffset, y);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyWidth, w);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyHeight, h);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyNormalizeFlag, normalize);
    editor.setProperty(id, C.kMaterialTypeCrop, C.kMaterialKeyRotateFlag, rotateWithCropCenter);
    return 0;
}

/**
*  设置一个图像类片段的锚点
*
* @param id 目标片段ID
* @param x x方向锚点,典型值为-0.5 - 0.5
* @param y y方向锚点,典型值为 -0.5 - 0.5
* @return 执行结果
*/
public int setAnchor(String id, float x, float y) {
    editor.setProperty(id, C.kPropertyCanvas, C.kMaterialKeyXAnchor, x);
    return editor.setProperty(id, C.kPropertyCanvas, C.kMaterialKeyYAnchor, y);
}

/**
* 设置一个图像类片段的旋转角度
*
* @param id 目标片段ID
* @param rotate 旋转角度,弧度
* @return 执行结果
*/
public int setRotate(String id, float rotate) {
    return editor.setProperty(id, C.kPropertyCanvas, C.kMaterialKeyRotate, rotate);
}


业务案例


当前Marvel已经接入了淘系多个业务产品中,包括手淘逛逛发布器、亲拍、MediaAI Studio、闲鱼、点淘等,主要的业务形态,包括拍摄后编辑、深度编辑、影集模板、口播编辑,另外还有一些水印处理、云剪辑等等之类的业务形态。随着Marvel的发展,未来还会支持其他业务,满足更多的业务诉求。



总结展望


本文以淘系的音视频非线性编辑引擎Marvel为主题,介绍了它的应用场景和引擎设计。音视频非线性编辑涉及到许多技术,包括编解码、音频处理、图像算法、图像渲染等等诸多方面,都是非常有意思的东西,每一块都可以单独成为一个技术方向。非线性编辑引擎不仅仅是基于这些技术的组装,也需要对这些技术进行升华,挖掘它们在非线性编辑中的业务价值。

在当前的规划中,后续我们除了功能与效率方面的工作外,还将持续针对Marvel引擎围绕着高效、稳定、灵活来进行技术上的演进,如增加预处理流程、进一步推进图像渲染&音频处理&预处理的插件化、增加片段间在时序上的相对布局等等。针对诸如远程资源下载、资源依赖管理、草稿动态转换、三方资源支持等非编辑核心能力的扩充,我们也将进行梳理,后续以插件的方式进行注入支持。

未来我们将继续以Marvel为编辑核心,推进淘系完善的短视频内容生产体系构建,保障淘系包括商家、买家、达人等诸多用户角色的短视频内容生产诉求。并在此基础上,沉淀技术,开放生态,持续为其他业务赋能。