1、Gstreamer design Overview
概括性介绍GStreamer的架构及Gstreamer内部工作机制。详细内容可以查看Gstreamer doc/markdown
1.1 Introduction
GStreamer由一些列库和插件组成,可用于实现各种多媒体应用:桌面播放器、音视频解码器、多媒体服务器、音视频转码器等。
上述应用通过pipeline构建,而pipeline又由单元(elements)构成。
单元是在多媒体流中执行一系列动作的对象:
- 读文件
- 不同格式之间的编解码
- 从硬件设备捕获图像
- 输出给硬件设备
- 多媒体流的混合或复用(mix or multiplex)
每个单元都有输入pads、输出pads,分别叫sink pads、source pads,单元之间通过pads连接构成pipeline,下面示例是ogg/vorbis 播放的pipeline:
+-----------------------------------------------------------+
| ----------> downstream -------------------> |
| |
| pipeline |
| +---------+ +----------+ +-----------+ +----------+ |
| | filesrc | | oggdemux | | vorbisdec | | alsasink | |
| | src-sink src-sink src-sink | |
| +---------+ +----------+ +-----------+ +----------+ |
| |
| <---------< upstream <-------------------< |
+-----------------------------------------------------------+
filesrc读取来自磁盘上的文件,oggdemux单元对数据解复用(demultiplexes 1转多),将压缩的音频数据流发送给vorbisdec单元,vorbisdec单元对压缩的数据解码并发送给alsasink单元,alsasink将解码后的数据发送给声卡播放。
Downstream、Upstream用于描述pipeline的方向,从source端到sink端传递叫Downstream,反之称为Upstream,数据流总是Downstream(source到sink端)
应用的本质是使用上述存在的单元构成pipeline,这在pipeline topic中有详细介绍。
应用不用去管理复杂的数据流/解码/转换/同步,只需调用pipeline对象的高层函数如:播放/暂停/停止。
应用还接受来自pipeline的消息和通知,如metadata,warning,error,EOS消息。
若应用需要对图像更多的控制,可以直接访问pipeline中的单元和pads。
1.2 Design overview
GStreamer设计目标:
- 对大量数据的快速处理
- 允许同时多线程处理
- 多种多媒体格式的支持能力
- 不同数据流之间的同步
- 应对多个硬件设备的处理能力
应用的能力依赖于构成单元的数量以及各单元在系统中的功能。
GStreamer core主要用于media agnostic,但仍可以提供一些单元特性用于描述media format。
1.3 单元(Elements)
pipeline最小构成为单元,每个单元提供一定数目的pads,可以是source 或sink pads。source pads提供数据,sink pads接收数据,下面是oggdemux单元由一个接收数据的sink pads,两个提供数据的source pads组成。
+-----------+
| oggdemux |
| src0
sink src1
+-----------+
单元包含四种状态:NULL
, READY
, PAUSED
, PLAYING
。NULL
和READY
状态下,单元不会处理数据。PLAYING
状态下处理数据,中间态PAUSED
用于在pipeline中预装载数据,使用gst_element_set_state()
函数改变状态。
单元状态之间的变换需要经过中间态,即处于READY
状态想要变换为PLAYING
状态,需要先变为中间态PAUSED
。
当单元状态变为PAUSED
,将激活单元的pads。首先激活的是source pads,然后sink pads。pads激活后,会调用激活函数,有些pads将开启GstTask线程或其它的机制,开始发送和接收数据(producing and consuming data
)
PAUSED
比较特别,因为它需要将数据预装载(preroll)到pipeline中,目的是用数据填充pipeline中的所有的连接单元以使得接下来的PLAYING
状态快速完成,有些单元甚至会出现已经接收完所有数据,但还没有完成PAUSED
状态的转换。
详细的单元状态的转变见:states
单元的分类:
source elements: pipeline中不接收数据,仅提供数据的单元
sink elements:不输出数据给其它单元,仅接收数据提供给外部设备。
transform elements: 需要完成数据流之间转变的单元,如编码器/解码器/转换器。
demuxer elements: 一个数据流变成多个数据流。
mixer/muxer elements: 多个数据流组合成单个数据流。
单元的其它分类方式 (see klass).
1.4 Bins
bin是单元的子类,其作用是作为其它单元的容器,这样多个单元可以组成一个单元。组成的一个单元叫bin。
bin相配合的子状态改变稍后给出解释,bin也会给单元分配事件和各种函数。
bin可以有自己的source 和 sink pads,通过镜像一个或多个自身的子pads。
如下图,有两个单元的bin,bin的sink pad由一个单元的sink pad镜像而来。
+---------------------------+
| bin |
| +--------+ +-------+ |
| | | | | |
| /sink src-sink | |
sink +--------+ +-------+ |
+---------------------------+
1.5 Pipeline
pipeline 特殊的bin子类,为其小孩提供下面特性:
- 为其所有小孩选择和管理全局时钟。
- 管理
running_time
,Running_time是pipeline消耗在PLAYING
状态以及用于同步的时间。 - 管理pipeline的延迟。
- 为单元提供与应用之间的通讯手段:
GstBus
- 管理单元的全局状态,如Errors、EOS
一般情况下,应用创建一个pipeline来管理应用中的所有单元。
1.6 Dataflow and buffers
GStreamer支持两种可能类型的数据流,push and pull model,push model,upstream单元通过调用sinkpad的方法发送数据给downstream单元;pull model,downstream单元通过调用source pad的方法向upstream单元请求数据。
大都数数据流的方式是push model,pull model可用于demuxer单元,也用于低延迟的音频应用中。
在pads之间传输的数据存储在Buffer中,buffer包含指向存储区域的指针以及对存储区域的描述metadata。
metadata包含:
数据的时间戳,何时捕捉的数据,何时播放数据...
数据的偏置,音频小样,视频帧..
数据持续时间。
数据其它特性的描述符号,如discontinuities or delta units.
其它仲裁metadata
通过连接单元之间的pad发送buffer,push model下,通过 gst_pad_push()
函数将buffer推送给相对应的pad;pull model下,通过gst_pad_pull_range()
函数向upstream的pad请求数据。
push model单元在发送buffer前,需要确认相配对的单元能够理解buffer的内容。询问相配对单元能够支持的格式,选择合适的格式,发送buffer前,通过CAPS事件先发送选择的格式。buffer发送之间的握手详见:negotiation
配对单元接收CAPS事件,将会检查其是否能够理解媒体类型(media type),当媒体类型不被接受时,配对单元将拒绝接受buffer。
gst_pad_push()
和gst_pad_pull_range()
函数都有返回值指示操作是否成功,返回值错误表示数据发送or数据接收没有成功,线程中初始化数据流的source单元将停止处理线程。
buffer的创建:gst_buffer_new()
或从buffer pool中请求可用的buffer:gst_buffer_pool_acquire_buffer()
,当使用第二种方法时,配对单元可以执行定制化的buffer分配算法。
The process of selecting a media type is called caps negotiation.
选择媒体类型的过程叫:caps negotiation
1.7 Caps
媒体类型(Caps)的描述使用包含key/value对的列表,key类型是string,value类型是single/list/range of int/float/string。
带有变量的Caps也可用于描述可能的媒体类型--可被pad处理。
1.8 Dataflow and events
数据流并行是一个事件流,不同于buffer,事件可在upstream和downstream之间同时传递,有些事件仅传送给upstream或downstream。
事件可用于指代数据流的特别条件,如EOS、通知插件的一些具体事件如 flushing or seeking。
一些事件必须用buffer流序列化,序列化的事件在buffer之间插入,没有序列化的事件跳到当前处理的任何buffer前面。
序列化事件的示例是TAG事件:buffer之间插入TAG事件来标识这些buffer的metadata。
非序列化事件的例子:FLUSH事件。
1.9 Pipeline construction
使用gst_pipeline_new()
创建pipeline,使用gst_bin_add()
and gst_bin_remove()
在pipeline中添加和删除单元
增加单元后,通过gst_element_get_pad()
函数找回单元的pads,使用gst_pad_link()
函数完成pads之间的连接。
当真实的数据流出现在pipeline中,一些单元会创建新的pads,当单元创建新的pad时,g_signal_connect()
函数会接收到一个通知,而后新创建的pads才能连接到其它未连接的pads上。
一些单元不能连接,因为他们的数据类型不一样, gst_pad_get_caps()
函数可以找回pad支持的数据类型。
下面创建了一个MP3播放器的pipeline,后续的example中将会使用该示例。
+-------------------------------------------+
| pipeline |
| +---------+ +----------+ +----------+ |
| | filesrc | | mp3dec | | alsasink | |
| | src-sink src-sink | |
| +---------+ +----------+ +----------+ |
+-------------------------------------------+
1.10 Pipeline clock
pipeline的一个重要作用:为所有的单元选择全局时钟。
该时钟的目的是提供严格的每秒增加值GST_SECOND
,各单元使用该时钟同步数据的播放。
在pipeline状态设置为PLAYING
前,pipeline会询问每个单元是否能提供时钟,时钟以下面的顺序选择:
- 应用选择时钟。
- source单元提供时钟。
- 其他单元提供时钟,从sink开始。
- 如果没有单元提供始终,使用pipeline的默认系统时钟。
对典型的播放pipeline,算法将会选择sink单元提供的时钟,如audio sink。
对捕捉pipeline,典型地使用数据产生器的时钟,大都数情况下不能控制其产生数据的数据率。
1.11 Pipeline states
当所有pads、所有信号连接好,pipeline可以放在PAUSED
状态以开始数据流。
当bin开始转变状态,将改变其所有孩子的状态。pipeline将从sink单元到source单元逐步改变孩子的状态,这将确保upstream单元发送数据给其它单元时,不会发生该单元还没有准备好接收的情况。
对MP3播放pipeline,单元的状态改变依次为alsasink,mp3dec,filesrc。
下面列出MP3 pipeline中各个单元的所有中间态转变,导致下面状态改变:
- alsasink 转变为
READY
:音频设备已准备好(probed)。 - mp3dec转变为
READY
:Nothing happens - filesrc转变为
READY
:文件已准备好 - alsasink 转变为
PAUSED
:音频设备已打开,alsasink作为sink,将返回ASYNC
(因为还不能接收数据) - mp3dec to
PAUSED
: mp3dec转变为PAUSED
:解码库已完成初始化。 - filesrc 转变为
PAUSED
:文件已打开,线程已开始push数据到mp3dec。
至此,数据流从filesrc--mp3dec--alsasink,因为mp3dec处于PAUSED
状态,其接收来自filesrc sinkpad端的数据,并对此压缩的数据进行解码为原始音频格式。
mp3解码器计算原始音频数据的采样率,信道数以及其他音频特性,并发送媒体类型的caps事件。
Alsasink接收该caps事件,解析caps,并完成自身配置,以能够处理媒体类型。
mp3dec 将解码后samples放到Buffer中,继而给buffer发送给下一个单元。
Alsasink接收带有samples的buffer,因为它接收samples的第一个buffer,完成状态转变为PAUSED状态,pipeline已prerolled,所有的单元都有samples
,此时Alsasink有能力提供时钟给pipeline。
alsasink处于PAUSED
状态,接收第一个buffer数据,alsasink blocks。mp3dec 和filesrc 在 gst_pad_push()
中也是blocks。
所有单元gst_element_get_state()
函数返回SUCCESS
,pipeline可以变为PLAYING
状态。
在进入PLAYING
之前,pipeline会选择一时钟,采样时钟的当前时间,成为base_time
,将该时间分配给所有单元,各单元利用该时钟的 running_time
base_time
实现同步,详见:synchronisation
而后发生下面的状态转变:
alsasink to
PLAYING
: 解码后的数据在音频设备中播放。mp3dec 转变为
PLAYING
:Nothing Happens。转变为
PLAYING
:Nothing Happens
1.12 Pipeline status
pipeline通过bus给应用发具体事件,bus是pipieline提供的一个对象,可通过gst_pipeline_get_bus()
配置。
bus能够被polled 或增加到glib mainloop。
bus分布在所有单元中,单元使用bus往上层推送消息,消息类型主要有:ERRORS
, WARNINGS
, EOS
, STATE_CHANGED
等。
pipeline以一特别的方式来处理来自单元的EOS
消息,当所有sink单元向上推送EOS
消息时,仅发送第一条给应用。
其他的获取pipeline状态包括:询问功能(Query functionality),通过执行函数 gst_element_query()
获取状态,询问的类型对于获取pipeline的位置及总时间信息是非常有用的,也可以用于询问支持的seeking格式和范围。
1.13 Pipeline EOS
当source滤波器遇到数据流的结束部分,将发出EOS事件给配对的单元,该事件将会通知downstream单元以及与其连接的所有单元,配对端sink pad收到EOS事件后,将不会接收任何数据。
单元在发送完EOS
事件后,将停止提供流线程。
EOS事件最终会到达sink单元,sink单元将在bus上发送EOS
消息,以通知pipeline一个特别的数据流已经完成。当所有的sink都向上发送EOS消息,pipeline仅向应用发送第一条EOS
消息。EOS
消息仅在PLAYING
状态下发送给应用。
当处于EOS
,pipeline保持PLAYING
状态,这是应用的职责对于PAUSE
or READY
pipeline。应用还可以发布seek。
1.14 Pipeline READY
当运行的pipeline从 PLAYING
变为READY
状态,pipeline接下来将发生:
alsasink转为 PAUSED
,alsasink将在下一个样本完成状态改变,若单元之前是EOS
,将不会等待样本完成状态转变。
- mp3dec to
PAUSED
: Nothing. - filesrc to
PAUSED
: Nothing
转换到中间态PAUSED
,将阻止所有的单元( _push()
函数),这是由于sink单元接收完第一个buffer后阻止后面的数据传输。
Some elements might be performing blocking operations in the PLAYING
state that must be unblocked when they go into the PAUSED state. This makes sure that the state change happens very fast.
有些单元在PLAYING
状态下也会出现阻止操作,在这些单元进入到PAUSED状态时,必须放开。确保状态能够快速转变。
下一个PAUSED
到READY
状态的改变,pipeline关闭,所有流线程需停止数据传输,过程如下:
- alsasink转为
READY
:alsasink放开(_chain()
函数),函数FLUSHING
返回值发送给配对的单元,sinkpad此时为非激活状态不能用于发送数据。 - mp3dec转为
READY
:pads进入非激活状态,当mp3dec离开_chain()
函数后完成状态变化。 - filesrc转为
READY
:pads进入非激活状态,线程停止。
由于downstream单元返回一个错误代码(_push()
返回FLUSHING
),upstream单元完成_chain()
函数。这个错误代码最终会反馈开始流线程的单元(filesrc),filesrc将停止线程并完成状态改变。
这个事件的顺序使得所有的单元放开,所有的流线程停止。
1.15 Pipeline seeking
pipeline seeking需要严格的操作步骤,以确保单元保持同步,seek以最小的延迟运行。
应用通过单元gst_element_send_event()
函数触发pipeline上seek事件,该事件可以是单元支持的任何格式的seek事件。
pipeline暂停pipeline以加速seek操作。
pipeline继而将seek事件发送给所有sink单元。sink前传seek事件给upstream直到某个单元对seek事件进行操作。所有的中间单元可以给seek偏置转为另外的格式,由此,解码器单元能够将seek转为时间戳的帧数。
当seek事件到达一个单元,将执行seek操作,单元相应地执行下面步骤:
- 发送
FLUSH_START
事件给配对的所有downstream和upstream单元。 - 确保流线程没有运行,因为步骤1,流线程将一直是停止。
- 执行seek操作。
- 发送
FLUSH
完成事件给所有配对的downstream和upstream。 - 发送
SEGMENT
事件通知新位置的所有的单元,完成seek操作。
步骤1中所有的downstream单元必须从所有的阻塞操作返回,必须拒绝接下来的任何buffer或者除FLUSH
外的其它事件。
第一个步骤确保流线程最终解除阻塞,继而第二步可以操作,此时pipeline的数据流全面停止。
步骤3,单元执行请求位置的seek操作。
步骤4,所有的配对单元又能够接收数据,新位置的数据流能够连续,FLUSH完成事件发送给所有配对单元,配对单元也能够接收新的数据,开启其数据流线程。
步骤5,通知新位置的所有单元,事件函数返回应用,流线程开始处理新的数据。
因为pipeline仍然处于PAUSED
状态,这将在sink端预处理下一个媒体样本,应用可以用_get_state()
函数等待预处理完成。
seek操作的最后一步是调节pipeline流的running_time
=0,设置pipeline返回PLAYING
状态。
MP3播放器示例中的seek事件顺序:
| a) seek on pipeline
| b) PAUSE pipeline
+----------------------------------V--------+
| pipeline | c) seek on sink
| +---------+ +----------+ +---V------+ |
| | filesrc | | mp3dec | | alsasink | |
| | src-sink src-sink | |
| +---------+ +----------+ +----|-----+ |
+-----------------------------------|-------+
<------------------------+
d) seek travels upstream
--------------------------> 1) FLUSH event
| 2) stop streaming
| 3) perform seek
--------------------------> 4) FLUSH done event
--------------------------> 5) SEGMENT event
| e) update running_time to 0
| f) PLAY pipeline