概述
这部分讲述了 GStreamer 的设计,并参考了不同主题更详细的说明。
本文档适用于希望全面了解 GStreamer 内部工作原理的人员。
介绍
GStreamer 是一组库和插件,可用于实现各种多媒体应用程序,包括桌面播放器、音频/视频录像、多媒体服务器、转码器等。
应用程序通过由elements组成的pipeline来构建的。一个 element是对多媒体流执行某些操作的对象,例如:
- 读取文件
- 在不同的格式之间解码或编码
- 从硬件设备捕获
- 渲染到硬件设备
- 混合或多路复用多个流
在 GStreamer 中,Elements具有称为sink pads和 source pads的输入和输出pads。 应用程序通过pads将element链接在一起构建pipeline。 下面是 ogg/vorbis 播放pipeline的示例。
+-----------------------------------------------------------+
| ----------> downstream -------------------> |
| |
| pipeline |
| +---------+ +----------+ +-----------+ +----------+ |
| | filesrc | | oggdemux | | vorbisdec | | alsasink | |
| | src-sink src-sink src-sink | |
| +---------+ +----------+ +-----------+ +----------+ |
| |
| <---------< upstream <-------------------< |
+-----------------------------------------------------------+
filesrc element 从磁盘文件上读取数据。 oggdemux element 对数据进行解复用并将压缩的音频流发送到 vorbisdec element。 vorbisdec element解码压缩数据并将其发送到 alsasink element。 alsasink element将采样发送到声卡进行播放。
下游(downstream )和上游(upstream )是用于描述pipeline中方向的术语。从源端到接收端被称为“downstream”,“upstream ”是从接收端到源端。数据流总是向下游传输。
应用程序的任务是使用现有elements构建上述pipeline。这在pipeline构建主题中进一步解释。
应用程序不需要管理任何复杂的实际数据流/解码/转换/同步(dataflow/decoding/conversions/synchronisation)等,但只调用pipeline对象上的高级函数,如PLAY/PAUSE/STOP。
该应用程序还从pipeline接收消息和通知,例如元数据、警告、错误和 EOS 消息。
如果应用程序需要对图形进行更多控制,则可以直接访问pipeline中的elements和pads。
设计概览
GStreamer 设计目标包括:
- 快速处理大量数据
- 允许完全多线程处理
- 处理多种格式的能力
- 同步不同的数据流
- 处理多种设备的能力
呈现给应用程序的功能取决于系统上安装的element数量及其功能。
GStreamer 核心被设计为与媒体无关,但为element提供了许多功能来描述媒体格式。
Elements
Pipeline中最小的构建块是element。一个element提供了许多pad,它们可以是source pad或 sink pad。 Sourcepad 提供数据,sinkpad 消费数据。下面是一个 ogg demuxer element的例子,它有一个接收数据的sink pad和两个产生数据的source pad。
+-----------+
| oggdemux |
| src0
sink src1
+-----------+
一个element可以处于四种不同的状态:NULL、READY、PAUSED、PLAYING。在 NULL 和 READY 状态下,element不处理任何数据。在 PLAYING 状态,它正在处理数据。中间 PAUSED 状态用于预卷pipeline中的数据。可以使用 gst_element_set_state() 执行状态更改。
一个element总是经历所有的中间状态变化。这意味着当一个element处于 READY 状态并被置于 PLAYING 时,它将首先经历中间 PAUSED 状态。
Element状态更改为 PAUSED 将激活element的pad。首先激活source pad,然后激活sink pad。当pad被激活时,pad激活功能被调用。一些 pad 会启动一个线程 (GstTask) 或其他一些机制来开始生产或消费数据。
PAUSED 状态很特殊,因为它用于预卷pipeline中的数据。目的是用数据填充pipeline中的所有连接的element,以便随后的 PLAYING 状态更改发生得非常快。因此,某些element在接收到足够的数据之前不会完成到PAUSED的状态。 Sink element需要在接收到第一个数据后才完成状态变为 PAUSED。
通常,element的状态变化由pipeline协调,如状态中所述。
存在不同类别的element:
- source elements:这些element不消耗数据,只为pipeline提供数据。
- sink elements:这些element不产生数据但将数据呈现给输出设备。
- transform elements:这些element将某种格式的输入流转换为另一种格式的流。编码器/解码器/转换器是示例。
- demuxer elements:这些element解析一个流并产生多个输出流。
- mixer/muxer elements:将多个输入流合并为一个输出流。
可以构造其他类别的element(参见 klass)。
Bins
bin 是element的子类,充当其他element的容器,以便可以将多个element组合为一个element。
Bin 协调它的子element的状态更改,稍后将对此进行说明。它还向element分发事件和各种其他功能。
通过镜像(ghostpadding)一个或多个孩子的pad,并添加到自己身上,bin可以拥有自己的source 和sink pad。
下面是一个带有两个element的 bin 的可视图。一个element的pad被镜像(ghostpadded )到bin。
+---------------------------+
| bin |
| +--------+ +-------+ |
| | | | | |
| /sink src-sink | |
sink +--------+ +-------+ |
+---------------------------+
pipeline
pipeline是一个特殊的 bin 子类,为其子类提供以下功能:
- 为其所有子element选择和管理全局时钟。
- 根据选定的时钟管理 running_time。 Running_time 是pipeline在 PLAYING 状态花费的时间,用于同步。
- 管理pipeline中的延迟。
- 通过 GstBus 为element提供与应用程序通信的方法。
- 管理错误和流结束等element的全局状态。
通常,应用程序会创建一个pipeline来管理应用程序中的所有element。
数据流和缓冲区
GStreamer 支持两种可能的数据流模型,推(push)和拉(pull)模型。在推模型中,上游element通过调用 sink pad 上的方法将数据发送到下游element。在拉模型中,下游element通过调用source pad上的方法从上游element请求数据。
最常见的数据流是推模型。分流器(demuxer )element可以在特定情况下使用拉模型。拉模型也可用于低延迟音频应用程序。
pad 之间传递的数据被封装在 缓冲区(buffers )中。缓冲区包含指向实际内存的指针以及描述内存的元数据。该元数据包括:
- 数据的时间戳,这是捕获数据的时间实例或应播放数据的时间。
- 数据的偏移量:媒体特定的偏移量,这可以是音频的采样或视频的帧。
- 数据的持续时间。
- 描述数据特殊属性的附加标志,例如不连续性或增量单位。
- 额外的任意元数据
当一个element希望将缓冲区发送到另一个元素时,使用链接到另一个元素的一个焊盘的一个焊盘来执行此操作。在推送模型中,使用 gst_pad_push() 将缓冲区推送到对等垫。在拉模型中,使用 gst_pad_pull_range() 函数从对等方拉出缓冲区。
在元素推出缓冲区之前,它应该确保对等元素可以理解缓冲区内容。它通过查询受支持格式的对等元素并选择合适的通用格式来实现此目的。然后,在推送缓冲区之前,所选格式首先通过 CAPS 事件发送到对等元素(请参阅协商)。
当一个 element pad 收到一个 CAPS 事件时,它必须检查它是否理解媒体类型。如果前面的媒体类型不被接受,该元素必须拒绝跟随缓冲区。
gst_pad_push() 和 gst_pad_pull_range() 都有一个返回值指示操作是否成功。错误代码意味着不应向该焊盘发送更多数据。在线程中启动数据流的源元素通常会在发生这种情况时暂停生产线程。
可以使用 gst_buffer_new() 或通过使用 gst_buffer_pool_acquire_buffer() 从缓冲池请求可用缓冲区来创建缓冲区。使用第二种方法,peer 元素可以实现自定义缓冲区分配算法。
选择媒体类型的过程称为上限协商。
Caps
媒体类型 (Caps) 使用键/值对的通用列表进行描述。键是一个字符串,值可以是 int/float/string 类型的 单个值/列表值/范围值。
没有范围值/列表值或其他可变值的Caps被称为是固定的,可以用来放在缓冲区上。
带有变量的Caps用于描述可由pad处理的可能的媒体类型。
数据流和事件
与数据流并行的是事件流。与缓冲区不同,事件可以向上游和下游传播。有些事件只向上游传播,有些只向下游传播。
这些事件用于表示数据流中的特殊条件,如 EOS,或通知插件的特殊事件,如刷新(flushing )或查找 (seeking)。
有些事件必须与缓冲区流序列化,有些则不需要。序列化事件被插入到缓冲区之间。非序列化事件跳转到当前正在处理的任何缓冲区之前。
序列化事件的一个示例是插入缓冲区之间以标记这些缓冲区的元数据的 TAG 事件。
非序列化事件的一个例子是 FLUSH 事件。
构建Pipeline
应用程序首先使用 gst_pipeline_new() 创建一个 Pipeline element。使用 gst_bin_add() 和 gst_bin_remove() 在pipeline中添加和删除element。
添加element后,可以使用 gst_element_get_pad() 检索element 的 pad。然后可以使用 gst_pad_link() 将pad链接在一起。
当实际数据流在pipeline中发生时,一些element会创建新的pad。使用 g_signal_connect() 可以在element创建pad时收到通知。然后可以将这些新的pad接到其他未链接的pad。
某些element无法链接在一起,因为它们操作不同的不兼容的数据类型。可以使用 gst_pad_get_caps() 检索 pad 可以提供或使用的可能数据类型。
下面是一个简单的
+-------------------------------------------+
| pipeline |
| +---------+ +----------+ +----------+ |
| | filesrc | | mp3dec | | alsasink | |
| | src-sink src-sink | |
| +---------+ +----------+ +----------+ |
+-------------------------------------------+
Pipeline clock
Pipeline的重要功能之一是为pipeline中的所有element选择一个全局时钟。
时钟的目的是以每秒一个 GST_SECOND 的速率提供一个严格递增的值。时钟值以纳秒表示。element使用时钟时间来同步数据的播放。
在pipeline设置为 PLAYING 之前,pipeline会询问每个element是否可以提供时钟。按以下顺序选择时钟:
- 如果应用程序选择了一个时钟,请使用该时钟。
- 如果source element 提供时钟,请使用该时钟。
- 从提供时钟的任何其他element中选择一个时钟,从sink开始。
- 如果没有element提供时钟,则pipeline使用默认系统时钟。
在典型的播放pipeline中,该算法将选择由诸如audio sink之类的sink element提供的时钟。
在录制(capture )类型的pipeline中,这通常会选择数据生成器的时钟,在大多数情况下,它无法控制它生成数据的速率。
pipeline状态
当所有的pad都连接好并且信号已经连接好后,pipeline可以进入 PAUSED 状态以启动数据流。
当 bin(以及pipeline)执行状态更改时,它将更改其所有子element的状态。pipeline将从sink element 到 source element 对其子element的状态进行更改,以确保没有上游element向尚未准备好接收它数据的element生产数据。
在 mp3 播放pipeline中,element的状态按照 alsasink、mp3dec、filesrc 的顺序改变。
遍历每个元素的所有中间状态,导致以下状态变化链:
- alsasink to READY:音频设备被探测
- mp3dec 到 READY:没有任何事情发生
- filesrc 到 READY:文件被探测
- alsasink 到 PAUSED:音频设备已打开。 alsasink 是一个sink并返回 ASYNC 因为它还没有收到数据
- mp3dec to PAUSED:解码库初始化
- filesrc to PAUSED:打开文件并启动一个线程将数据推送到 mp3dec
此时数据从 filesrc 流向 mp3dec 和 alsasink。由于 mp3dec 已暂停,它在sink pad 上接收 filesrc 的数据并开始将压缩数据解码为原始音频采样。
mp3 decoder 计算原始音频采样的采样率、通道数和其他音频属性,并发送带有媒体类型的 caps 事件。
然后 Alsasink 接收 caps 事件,检查 caps 并重新配置自身以处理相关媒体类型。
mp3dec 然后将解码后的采样放入 Buffer 并将此缓冲区推送到下一个element。
Alsasink 接收带有音频采样的缓冲区。由于它收到了第一个音频采样缓冲区,因此它完成了到 PAUSED 状态的状态更改。此时pipeline已预卷,所有element都有音频采样。 Alsasink 现在还能够为pipeline提供时钟。
由于 alsasink 现在处于 PAUSED 状态,它在接收第一个缓冲区时会阻塞。这有效地使mp3dec 和 filesrc 阻塞在 gst_pad_push() 中的 。
由于现在所有element都从 gst_element_get_state() 函数返回 SUCCESS,因此可以将pipeline 置于 PLAYING 状态。
在进入 PLAYING 之前,pipeline 选择一个时钟并对时钟的当前时间进行采样。这是base_time。然后将这个时间分配给所有element。然后element可以使用缓冲区 running_time base_time 与时钟同步(另请参见同步)。
然后发生以下状态更改链:
- alsasink to PLAYING:采样播放到音频设备
- mp3dec 到 PLAYING:没有任何事情发生
- filesrc 到 PLAYING:没有任何事情发生
pipeline状态
pipeline通过总线通知应用程序在pipeline中发生的任何特殊事件。总线对象由pipeline提供,可以使用 gst_pipeline_get_bus() 检索。
总线可以被轮询或添加到 glib 主循环。
总线分配给添加到pipeline的所有element。这些element使用总线来发布消息。存在各种消息类型,例如 ERRORS、WARNINGS、EOS、STATE_CHANGED 等。
Pipeline以特殊方式处理从element接收到的 EOS 消息。只有当所有接收器element都发布了 EOS 消息时,它才会将消息转发给应用程序。
获取pipeline状态的其他方法包括可以在pipeline上使用 gst_element_query() 执行的查询功能。这种类型的查询对于获取有关pipeline当前位置和总时间的信息很有用。它还可以用于查询所支持的搜索格式和范围(seeking formats and ranges)。
流水线EOS
当source filter 遇到流的末尾时,它会向 peer element发送一个 EOS 事件。然后,此事件将向下游传播到所有连接的element,以通知它们 EOS事件。在sink接收到 EOS 事件后,该element不应再接受任何数据。
提供流线程的元素在发送 EOS 事件后停止发送数据。
EOS 事件最终会到达 sink 元素。然后接收器将在总线上发布一条 EOS 消息,以通知管道特定流已完成。当所有接收器都报告了 EOS 时,管道将 EOS 消息转发给应用程序。 EOS 消息只转发给处于 PLAYING 状态的应用程序。
在 EOS 中,pipeline保持在 PLAYING 状态,将pipeline设置为 PAUSE 或 READY状态是应用程序的责任。例如,应用程序还可以发出定位(seek)。
pipeline就绪
当把正在运行的pipeline从 PLAYING 设置为 READY 状态时,pipeline中会发生以下操作:
- alsasink 到 PAUSED: 在下一个采样,alsasink 阻塞并完成状态更改。如果element接收到了 EOS,则不会等待下一采样就完成状态更改。
- mp3dec 到 PAUSED:没有事情
- filesrc 到 PAUSED: 没有事情
进入中间 PAUSED 状态将所有element阻塞在_push() 函数中的。发生这种情况是因为sink element在它接收到的第一个缓冲区上阻塞。
某些element可能在 PLAYING 状态下执行阻塞操作,当它们进入 PAUSED 状态时必须解除阻塞。这确保状态更改发生得非常快。
在下一个 PAUSED 到 READY 状态更改中,pipeline必须关闭,并且所有流线程并且停止发送数据。这按以下顺序发生:
- alsasink 到 READY: alsasink解除_chain()函数的阻塞,并向peer element 返回 FLUSHING 返回值。 sinkpad 失效并且无法用于发送更多数据。
- mp3dec 到 READY:pads 失效并且当 mp3dec 离开它的 _chain() 函数时完成态改变。
- filesrc 到 READY:pads 失效并且线程被暂停。
上游element结束了它们的 _chain() 函数,因为下游element从 _push() 函数返回了一个错误代码 (FLUSHING)。这些错误代码最终返回到启动流线程(filesrc)的element,该element暂停线程并完成状态更改。
这个事件序列确保所有element都不阻塞,所有流线程都停止。
管道定位(seeking)
在pipeline中seek需要非常特定的操作顺序,以确保element保持同步并且以最小的延迟执行seek。
应用程序使用pipeline element上的 gst_element_send_event() 在pipeline 上发出seek事件。该事件可以是element支持的任何格式的seek事件。
pipeline首先暂停pipeline以加速查找操作。
然后pipeline向所有sink element发出seek事件。然后sink向上游转发seek事件,直到某个element可以执行seek操作,这通常是source或demuxer element。例如,所有中间element都可以将请求的seek偏移量转换为另一种格式,这样decoder element可以将seek转换为帧号和时间戳。
当seek事件到达将执行seek操作的element时,该element执行以下步骤。
- 向所有下游和上游 peer element发送 FLUSH_START 事件。
- 确保流线程没有运行。由于步骤 1),流线程始终是停止的。
- 执行seek操作
- 向所有下游和上游peer element发送 FLUSH done 事件。
- 发送 SEGMENT 事件以通知所有element的新位置并完成seek。
在步骤 1) 中,所有下游element都必须从任何阻塞操作中返回,并且必须拒绝任何与 FLUSH done 不同的其他缓冲区或事件。
第一步确保流线程最终解除阻塞并且可以执行步骤 2)。此时,数据流在pipeline中完全停止。
在步骤 3) 中,element执行到请求位置的seek。
在步骤 4) 中,允许所有peer element再次接受数据,并且流可以从新位置继续。 FLUSH done 事件被发送到所有peer element,以便它们再次接受新数据并重新启动它们的流线程。
步骤 5) 通知所有element在流中的新位置。之后,事件函数返回给应用程序。并且流线程开始产生新数据。
由于pipeline仍处于暂停状态,这将在sink中预卷下一个媒体采样。应用程序可以通过在pipeline上执行 _get_state() 来等待此预卷完成。
然后,seek操作的最后一步是将pipeline的流running_time调整为0,并将pipeline设置回PLAYING。
| 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