【Gstreamer】PUSH/PULL mode生动解析

gstreamer 在处理资料的流动有两种主要的模式,一个是「推」,一个是「拉」。两种模式需要实作的 routine 不同,在对资料的操作 (manipulation) 上的重点也不一样,很容易被搞得摸不清方向(其实我到现在还是有很多没搞懂的地方…)。首先先解释一下两者的不同。

「推」模式就是由上游的插件控制资料的大小、流速,向下「推」到下游的插件,所以下游的插件并不会事先知道有多少资料会被送进来,它就必须先准备一个缓衝区来承接资料,然后判断缓衝区裡的资料是否足够拆解出一个压缩单位的资料,够的话就把资料切割出一个固定大小送给解码器,剩下的资料要留著和下一笔流进来的资料做连接。


「拉」模式则是需要自己控制资料大小、流速,告诉上游的插件说自己要多少资料,从几分几秒开始读,自己控制速度、大小等等变数,把资料「拉」进来。因为要流进来的资料量 (举例来说,media-object 的 size、chunk size、packet size) 自己可以控制,就不需要设计一个缓衝区来放资料。

通常,「拉」模式会用在 demuxer,而「推」模式用在其他插件,所以 gst-template 提供的例子是「推」模式的写法。_chain() 函式就是让上游插件把资料送进来的接口,当资料开始流动的时候 (完成启动阶段(activation stage)后,启动的部份留待后述。) 会直接唤起初始阶段时向 pad 注册的 chain 函式,这个函式的介面 (GstPadChainFunction) 是已经被定义好的,其中一个变数是 GstBuffer 的指标,资料就被塞在这个指标所指向的记忆体空间。我们便可以透过注册进去的函式,取得操作这段资料的 handle 。

Gstreamer 在处理资料流有四个状态:Null, Ready, Pause, Playing 按顺序切换。也就是说,刚开始播放一个档案时状态变化是: Null –> Ready –> Pause –> Playing,当播放结束要释放 pipeline 的顺序就是原路走回去:Playing –> Pause –> Ready –> Null。我们写的这个 mp3dec 插件是要把 mpeg audio decoder libmad 包装为 gstreamer 插件,所以在开始播放档案之前必须先把插件初始化 (比如说,设定 member variable 的初始值,初始化 gstreamer 的其他元件等等),当然,也要先初始化 libmad。初始化的动作一般来说,应该要放在 Null 转到 Ready 的阶段,或 Ready 转到 Pause 的阶段,绝对不可能是在 Pause 转到 Playing 的阶段,因为 Pause 和 Playing 两个状态是切换播放模式用的 (如:暂停、快进、Seeking) 。


为了处理刚提到的状态切换,我们要注册一个 _change_state() 函式。


 1: static GstStateChangeReturn
 2: gst_mp3dec_change_state(GstElement* element, GstStateChange transition)
 3: {
 4: GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
 5: Gstmp3dec *dec;
 6: dec = GST_MP3DEC(element);
 7: 
 8: switch(transition)
 9: {
 10: case GST_STATE_CHANGE_NULL_TO_READY:
 11: mad_frame_init(&dec->frame);
 12: mad_stream_init(&dec->stream);
 13: mad_synth_init(&dec->synth);
 14: break;
 15: default:
 16: break;
 17: }
 18: 
 19: ret = parent_class->change_state(element, transition);
 20: if(ret == GST_STATE_CHANGE_FAILURE)
 21: return ret;
 22: 
 23: switch(transition)
 24: {
 25: case GST_STATE_CHANGE_READY_TO_NULL:
 26: gst_mp3dec_reset(dec);
 27: break;
 28: default:
 29: break;
 30: }
 31: return ret;
 32: }
 33: 
 34: static void gst_mp3dec_clas_init()
 35: {
 36: ...
 37: gstelement_class->change_state = gst_mp3dec_change_state;
 38: ...
 39: }
如刚所说,当状态从 NULL 转到 READY 时 (GST_STATE_CHANGE_NULL_TO_READY),插件要做初始化,配置记忆体等。反过来当状态从READY转到NULL时 (GST_STATE_CHANGE_READY_TO_NULL),就要释放资源。为了避免当主要的执行续(main thread)还在运作时,就因为收到「停止」的指令,从 PLAYING 切进 NULL ,把资源都给释放掉,所以状态转换要分成两个 switch-case 来处理。
我们可以试著讨论一下 pipeline 如此处理状态切换的理由是什麽。想像你手上有一个滤水器,一个水桶的污水和一个乾淨的水壶。当你要开始过滤污水的时候,你会不会先检查水壶已经正确地接在滤水器的另一端了?要开始把污水往下倒时,会不会先把滤水器的开关打开,会吧?水壶和滤水器都「READY」了以后,才开始把污水往下倒。如果你使用滤水器的方法和我不同,请麻烦接受这个「由下而上READY」的想法,因为这是 gstreamer 在做开关控制的精神。
反过来看,如果要停止滤水,该是怎样的顺序?没错,把上面过滤的顺序反过来。先停止倒污水,再关闭滤水器,最后才盖上水壶。这样的流程要怎麽用程式码表达呢?

Gstreamer 只提供了一个函式来处理整个 pipeline 开始和结束的动作,在 mp3dec 这个例子中,就是我们注册进去的 gst_mp3dec_change_state。只有一个函式的话,还要兼顾「开的时候下游先开,关的时候上游先关」的原则,最简单的做法就是:播放初始时先替自己做初始化,准备好了以后通知上游。播放结束时先通知上游,再释放自己的资源。所以,就会出现上面那段程式码的写法。

当 pipeline 的状态被切换到 PLAYING 的时候,gstreamer 会开始做 preroll (提取影音资料进缓衝区),此时 _chain() 函式就会被触发。主要的资料处理工作就是在 _chain() 裡完成,在「拉」模式的情况下,主要的资料处理工作则是在 _loop() 裡完成,以后会说明。因为 _chain() 裡面牵涉到 mpeg audio 解码的程式,和 libmad 调用的部份、处理缓衝伫列等等比较複杂,将另开篇幅说明。

还不知道 pad 是什麽没关系,先想像它是插件的「开口」就好;所谓的 pipeline 的箭头是有方向性的,资料从源头 (档案、网路…等) 读取出来后,从读取的插件开始(即:file-source),到播送的插件出去(即:audio-sink 和 video-sink)。

透过插件的「开口」,资料才能在插件之间流动,就像滤水器的进水阀和出水阀,控制流进流出的水量、速度等等。不过 gstreamer 的水阀比较複杂一点,它必须再去判断多媒体资料流的属性,动态地决定输入的多媒体档案要用哪一个滤水器来承接。在这裡水阀就是GstPad ,而标示水阀的「属性」就是GstCaps。 进水阀我们称为「sink pad」,出水阀我们称为「source pad」,所以按上图来看,file-source 没有「安装」「sink pad」是因为他在进水的那一条路是透过系统的 file I/O 来处理,不属于 gstreamer pad 的范畴;同样的 audio-sink 和 video-sink 没有「安装」「source pad」是因为在播放声音和影像的部份是透过系统的 A/V renderer。而在中间的插件们,最基本的型态是一个进水(后称 sinkpad )一个出水(后称 srcpad ),像 decoder ;而 demuxer 要把 audio/video (或更多,视封装格式而定) 资料拆开给各自的解码器,就会有一个 sinkpad ,多个 srcpad ,因为责任重大,demuxer 写起来也比较複杂。

设定这些属性的目的就跟前述一样,让 gstreamer 在自动产生 pipeline 的时候可以按照我们设定的格式找到正确的插件来处理资料。(想像一下滤水器的进入出入阀标示著这个是滤工业用水、那个是滤农业用水、另一个是滤家庭用水,口径多少、每单位吃水量多少…等等等,如此就算滤水器的功能一样,而相对应的口径、水量不符合,gstreamer 也不会接错。)

下面这段,我觉得真他妈写的好,看来此主真的是研究透彻,台湾人的确软件水平的确要比我们高一些:


然而,这边设定的 caps 只是一个样板,告诉上下插件输入和输出资料的格式及相关属性的「范围」,做为建立 pipeline 时参考的依据,当档案开始播放时,真正的资料流的格式、属性要等解码完才知道( 其实这点我觉得不对,pause之前我认为是这样,因为这个时候没有真正解流或文件,无法得知真正info,此刻只是创建,并未决定。但pause后,其实数据就已经open,并尝试开始解析几帧数据find stream info等操作了,然后根据尝试decode的几帧查找和link 对应的pads.只是clock没有走动,preroll这个时候也没有render,所以没有显示.故play之前所有的pads都已经查找并且连接完毕,如果等play的时候再查找,肯定用户能感觉出来,个人见解 )。换言之,caps 的设定不一定是在 template 裡写死就好,有时要另外动态产生运行时对应的 caps 并指派给 pad ( 包括 sinkpad 和 srcpad )。

设定这些属性的目的就跟前述一样,让 gstreamer 在自动产生 pipeline 的时候可以按照我们设定的格式找到正确的插件来处理资料。(想像一下滤水器的进入出入阀标示著这个是滤工业用水、那个是滤农业用水、另一个是滤家庭用水,口径多少、每单位吃水量多少…等等等,如此就算滤水器的功能一样,而相对应的口径、水量不符合,gstreamer 也不会接错。)


在处理 sinkpad 和 srcpad 的程式都还没写之前就先设定 caps 其实并没有具体的功能,但我觉得这样解释比较不会搞不清楚或混淆 caps 的目的和重要性。
原因很简单,就是 gstreamer 发现 mad 的输出阀 (srcpad) 和 mp3dec 的输入阀 (sinkpad) 的 caps 不符合。所以跑都不跑就直接跳掉了。

你可能感兴趣的:(Gstreamer)