本文主要介绍金山云Android推流、短视频SDK设计中,为保证SDK的灵活性、可扩展性,在SDK组件化方向上所做的一些探索。
成熟的PC端多媒体架构简介
PC诞生之初,就有了强烈的多媒体处理需求,在几十年发展中,比较知名的几个多媒体框架有:
- 微软的DirectShow
- 开源跨平台的GStreamer
- FFMPEG
- VLC
其中,FFMPEG更偏重于提供muxer/demuxer, encoder/decoder等实用稳定的多媒体组件,VLC更偏重于提供ALL IN ONE的软件产品,其框架更多的是为特定的应用场景服务,灵活性及扩展性均不及DirectShow和GStreamer.
DirectShow和GStreamer的组件化设计
DirectShow与GStreamer均为组件化设计的多媒体框架,具体工作均交由各个组件来实现。
DirectShow的Filter Graph
微软提供了可视化的组件编辑工具GraphEdit,借助该工具,我们可以通过直观的方式将各个DirectShow的组件连接起来,并对实际效果进行预览。
比如下图的连接方式就实现了一个基本的视频文件播放器:
根据上图,我们可以看到,一个典型的视频播放器包含一个视频源分离模块(Demuxer), 一个视频解码模块,一个音频解码模块,一个视频渲染模块,一个音频渲染模块。在DirectShow中,这些模块被称为Filter,连接起来的各个Filter组成了一个Filter Graph。
各个Filter包含不同类型与数目的引脚,通过引脚间的连接,实现数据流在不同模块间的传递。
这些引脚在DirectShow中称为Pin, 其中产生数据的Pin被称为Source Pin,接受数据的Pin称为Sink Pin。
例如分离模块中包含音视频两个Source Pin, 解码模块包含一个Sink Pin和一个Source Pin, 渲染模块只有一个Sink Pin。
当然我们也可以通过选择组合不同的Filter组成新的Filter Graph来达成不同的功能,或者增添、更改当前Filter Graph中的Filter来动态调整Filter Graph的功能特性。
GStreamer的Pipeline
GStreamer中存在类似的组件化结构,例如下图的Pipeline实现了一个简单的ogg音频文件播放器:
如图中所示的source, demuxer, decoder, output模块,在GStreamer中被称为Element, Element上的引脚被称为Pad, 输入输出引脚分别被称为Source Pad和Sink Pad,而连接起来的各个Element则组成了一个Pipeline。
GStreamer同样支持使用不同的Element及连接方式来组成不同的Pipeline,以及对其中的Element进行增添、改动来调整Pipeline的功能特性。
背后的工作
前面我们看到了DirectShow和GStreamer直观、灵活的组合方式,以及强大的扩展性,但要实现这些特性是需要框架完成大量的配套工作的。
模块间连接时的协商过程。
在多媒体处理中,存在着多种数据类型,例如未解码的视频数据(其中又存在多种编码格式HEVC/AVC/VP9等),解码后的视频数据(又包含RGB/I420/YV12/NV12等),不同模块能够处理的数据类型是不同的,因此两种框架均实现了完善的协商过程。
例如,对于不支持的连接方式,抛出错误,或者智能添加一个转换模块来完成连接。模块的动态添加及移除处理。
例如在播放或者编辑视频的过程中,需要添加、改变或者移除一种特效Filter,就需要对已连接的模块进行动态重建。
两种框架均实现了该功能,不过为此也做了大量的工作,例如模块在变动过程中的状态处理、数据流处理等。模块间的数据传送。
一般存在两种方式,一种为push模式,另一种为pull模式。
两种框架均对push及pull模式做了支持。例如上面GStremer框架下的ogg播放器,ogg-demuxer的sink pad就以pull模式从file-source拉取数据,后继模块则均以push模式运行,由上一级模块的src pad将数据推送到后一级的sink pad。状态控制和事件响应。
GStreamer中,要控制Pipeline的开始、暂停、停止状态,只需控制Pipeline的状态,GStreamer框架内部会实现对各个子Element的状态切换。
对Pipeline运行过程中的seek操作也是类似,框架内部会将SEEK事件发送到Pipeline的所有Sink Pad以完成seek操作。错误及消息处理。
GStreamer中每个Pipeline均包含一个传递错误及消息的Bus,每个Element会将其本身产生的错误及事件消息放进该Bus中,上层应用通过监听Bus中的事件来进行必要的错误及事件消息的处理。音视频同步。
两种框架中,均提供了Clock选择机制,被选中的Clock可以被各个模块作为参考,用来控制数据的发送节奏,特别是音视频Render模块,可以使用相同的参考时钟来控制渲染时机,以达到音视频同步播放的效果。
金山云Android多媒体SDK的架构设计
金山云Android多媒体SDK是以在保证性能前提下提供足够灵活的扩展性为目标的。为此,我们采用将SDK中的各个功能模块组件化,然后根据应用场景进行组装的方式来达成。
以下图为例,展示了推流SDK中各个模块的典型Pipeline结构:
图中的各个模块通过KSYStreamer类组合在一起,实现完整的直播推流功能。而通过不同的组织方式,又可以组成一个短视频合成SDK,如下图所示:
框架中对模块的形式,模块间的组织方式的处理参考了DirectShow和GStreamer框架中的一些概念,不过框架最初只是为了推流功能所设计,为兼顾实现难度及性能,做了较大幅度的简化及限制。
基于Pin的模块间连接方式
在金山云Android多媒体SDK中,参照DirectShow及GStreamer的概念,以简化模块间连接为目的,引入了Pin的概念,简要介绍如下:
在搭建推流Pipeline的时候,各个模块之间的连接使用 SrcPin 和 SinkPin 来完成。
- 一个Module包含若干个Pin, Module之间的连接由Pin来实现
- Pin包含SrcPin和SinkPin, 分别产生和消耗数据流
- SrcPin及SinkPin均是泛型类,创建时需要指定数据格式,相同数据格式的Pin才可以连接,例如:
SrcPin<ImgTexFrame> -> SinkPin<ImgTexFrame>
SrcPin<ImgBufFrame> -> SinkPin<ImgBufFrame>
SrcPin<AudioBufFrame> -> SinkPin<AudioBufFrame>
- 一个SrcPin可以连接多个SinkPin, 一个SinkPin只能跟一个SrcPin连接;
- 所有连接或断开连接的操作均由SrcPin端操作;
Pin的相关操作
- 调用SrcPin的connect接口连接两个模块
public void connect(SinkPin<T> sinkPin)
- 调用SrcPin的disconnect接口断开连接
// 断开所有已连接的SinkPin, recursive为true时表示需要递归断开后面所有已连接的模块
public void disconnect (boolean recursive)
// 断开指定的某个已连接的SinkPin,recursive为true时表示需要递归断开后面所有已连接的模块
public void disconnect (SinkPin<T> sinkPin, boolean recursive)
SrcPin调用disconnect后,SinkPin端可以收到onDisconnect事件
// 源端已断开连接,recursive为true时需要release当前模块,并递归断开后面所有已连接的模块
public abstract void onDisconnect (boolean recursive)
-
处理onFormatChanged
该接口表示数据格式的改变,源端数据初始化完成及发生改变时均需要触发改事件,Sink端一般需要在该回调中进行一些初始化的工作。- 包含SrcPin的模块需要在合适的时机触发onFormatChanged;
- 包含SinkPin的模块需要根据需要处理SrcPin触发的onFormatChanged事件。
-
处理onFrameAvailable
- 包含SrcPin的模块需要在新的一帧数据ready时触发onFrameAvailable;
- 包含SinkPin的模块在onFrameAvailable中可以获取新的一帧数据。
其他部分的处理方式
模块间连接的兼容检测。
参照上一节对Pin的介绍,模块间连接的兼容检测是通过Pin中所包含的数据类型来确定的,这个检测在编译阶段就完成了。
不过,即便对于同一种数据类型,例如ImgBufFrame,也包含I420, RGBA, NV12等不同的色彩格式,可以处理ImgBufFrame的模块不一定支持所有的色彩格式,这时就需要使用者在组织模块的时候留意,或者在模块间显式加入一个通用的色彩空间转换模块。模块的动态添加及移除处理。
在已经运行的Pipline的A、B模块间加入模块C时,以直观的方式,先断开A、B间的连接,然后使用A的SrcPin连接C的SinkPin,以C的SrcPin连接B的SinkPin。移除模块的方式也是类似的处理。
模块的动态变动一般发生在切换音视频滤镜,或者切换编解码方式的时候,SDK针对这种通用场景,实现了滤镜管理类,以及Codec管理类以方便使用。模块间的数据传送。
参照上节Pin的相关接口,这里对数据流的传递仅实现了push模式,也就是数据一定是从上一级推到下一级。如果下一级模块要实现媒体流的步进控制,可以通过阻塞上一级输入的方式来实现。状态控制及消息处理等。
框架中并未对Pipeline及其中各个模块的状态、消息及错误信息提供一个统一的处理方式,需要开发者在组装各个模块时,分别控制及监听各个模块的状态、消息及错误信息等。音视频同步。
框架在构建时仅针对直播推流场景,本身并未实现音视频同步的机制,时钟部分则直接使用System.nanoTime()调用获取系统时间作为系统时钟源。
在需要进行音视频同步的模块中,可以通过阻塞输入过快的媒体源来达成对上级模块的节奏控制。
总结与改进
上述框架是在构建推流端SDK时所设计,为Android直播推流SDK提供了灵活强大的扩展能力,不过依然存在很多可优化部分。
需要完善对短视频SDK场景的支持
数据流部分加入pull模式的支持。
短视频应用场景下,是以本地文件作为视频源的,其读取文件以及demux过程不会成为整个处理过程的瓶颈,另外,对解码节奏的控制交由对解码后数据进行处理的模块来进行更为合理,框架中加入pull模式支持对于短视频应用的构建更为方便。加入全局的Clock机制来实现音视频同步。
短视频预览、编辑、转场效果等场景下有音视频同步的需求,在框架中加入全局的时钟机制能够简化应用的复杂度。
简化模块实现以及模块组装的工作
考虑引入模块组装管理类(Pipeline类),连接、移除模块时不再直接通过SrcPin进行,而是通过Pipeline类代理实现。通过这种方式,可以达到:
- 对于状态切换及资源释放,只需要操作Pipeline的相应接口,不需要对逐个模块进行操作(特殊场景下依然可以逐模块控制)。
- 可以即时获取当前Pipeline的链接结构,方便debug。
- 可以将GLRender, Clock等可能全局需要的参数自动设置到各个模块,以简化模块组装的过程。
- 能够将各个模块的异步事件、错误消息等汇集到一处,应用构建者只需要监听统一的接口。