Android多媒体SDK中的组件化设计思想

本文主要介绍金山云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的组件连接起来,并对实际效果进行预览。
比如下图的连接方式就实现了一个基本的视频文件播放器:

DirectShow框架下的视频播放Pipeline

根据上图,我们可以看到,一个典型的视频播放器包含一个视频源分离模块(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音频文件播放器:

GStreamer框架下的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结构:

推流SDK典型Pipeline结构图

图中的各个模块通过KSYStreamer类组合在一起,实现完整的直播推流功能。而通过不同的组织方式,又可以组成一个短视频合成SDK,如下图所示:

image.png

框架中对模块的形式,模块间的组织方式的处理参考了DirectShow和GStreamer框架中的一些概念,不过框架最初只是为了推流功能所设计,为兼顾实现难度及性能,做了较大幅度的简化及限制。

基于Pin的模块间连接方式

在金山云Android多媒体SDK中,参照DirectShow及GStreamer的概念,以简化模块间连接为目的,引入了Pin的概念,简要介绍如下:

在搭建推流Pipeline的时候,各个模块之间的连接使用 SrcPinSinkPin 来完成。

  • 一个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场景的支持

  1. 数据流部分加入pull模式的支持。
    短视频应用场景下,是以本地文件作为视频源的,其读取文件以及demux过程不会成为整个处理过程的瓶颈,另外,对解码节奏的控制交由对解码后数据进行处理的模块来进行更为合理,框架中加入pull模式支持对于短视频应用的构建更为方便。

  2. 加入全局的Clock机制来实现音视频同步。
    短视频预览、编辑、转场效果等场景下有音视频同步的需求,在框架中加入全局的时钟机制能够简化应用的复杂度。

简化模块实现以及模块组装的工作

考虑引入模块组装管理类(Pipeline类),连接、移除模块时不再直接通过SrcPin进行,而是通过Pipeline类代理实现。通过这种方式,可以达到:

  • 对于状态切换及资源释放,只需要操作Pipeline的相应接口,不需要对逐个模块进行操作(特殊场景下依然可以逐模块控制)。
  • 可以即时获取当前Pipeline的链接结构,方便debug。
  • 可以将GLRender, Clock等可能全局需要的参数自动设置到各个模块,以简化模块组装的过程。
  • 能够将各个模块的异步事件、错误消息等汇集到一处,应用构建者只需要监听统一的接口。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 最近加入了一个写作社群,和77个小伙伴一起,每7天写一篇,持续写7年。群里有不少朋友刚刚开始写,苦恼于不知道该写点...
    小野与玫瑰阅读 1,445评论 0 12
  • 感觉沉重吗?能说是一个女人的悲哀与不幸吗?似乎又不完全准确。 洞察世事的池莉,在她的小说《所以》里,用看似轻松诙谐...
    云之风舞阅读 432评论 0 0
  • 我害怕被人看出我穷 内心空洞 思想龌龊 只想做个游手好闲的混蛋 我很多余 我想的很多余
    我睡过文艺阅读 216评论 0 0
  • 高一还是高二,放假回家,弟弟出来车站接我。到家,把行李放好,就到了该吃饭的时候了,爷爷奶奶叫吃饭了。爷爷奶奶还是住...
    和尚的尼姑阅读 164评论 0 0