Gstreamer Plugin 编写 之 入门

原文链接:http://blog.csdn.net/alex_xhl/article/details/6061764(原文分为三篇,我合成了一篇)

 

1.踏出 gstreamer plugin 的第一步       

搞 gstreamer 插件已经好一阵子,好不容易勉强算的上知道怎么去写一个插件,严格说来看 gstreamer 写的 plugin writer’s guide 应该是看不懂要怎么接著去叫 gstreamer 做事的,网路上找的到的资料,也绝大多数是在讲 integration layer (也就是纯 player 啦) 的部份,而对于 development layer (也就是 pipeline 里的 element) 该怎么撰写付之阙如。我打算趁手上的 wmv 插件告个段落时写个笔记,算是为了交流,也算是帮自己重新确认观念无误。

[转]踏出 <wbr>gstreamer <wbr>plugin <wbr>的第一步

这回我想拿 mp3 档案做例子,用一个比较普遍而且广泛应用的格式做练习有很多好处,一来没有影像,就先省去同步的问题,也不会有 cpu / bandwidth 不够的问题 (因为 video 的 bitrate 比 audio 高出很多);二来测试档案满地爬,而且横跨多种不同参数的压缩格式,更好的是可以互相参照的播放器也是满地爬(一不小心就踩到的程度…搭捷运时年轻人几乎人手一台 player),抓虫或对照功能时很好用。

gstreamer 提供了一个 command line 建立 pipeline 的工具:gst-launch。给不同的参数可以自动或手动的方式去播放一个多媒体档,这个工具说方便很方便,说不方便也的确有点麻烦。方便是一个指令就可以叫它开始播档案,省去图型化介面的慢和滑老鼠的动作;不方便是因为它除了 play 以外没有别的 navigation command,不像 mplayer 还有给 hotkey 快转 (快转对于看谜片来说是很重要的呀!!)。至于所谓的 pipeline,长得就像这样

[转]踏出 <wbr>gstreamer <wbr>plugin <wbr>的第一步

箭头和方块组成的结构就称为 pipeline,而每个方块 (element) 都负责某一部份的资料处理,称为 element。这和 DirectShow 的 graph 是相当神似的。有 DirectShow 基础的人应该会比我还快了解 gstreamer 吧。

总之,自动建立 pipeline 的指令是如此:

gst-launch playbin uri=file:///path/to/file.mp3

而手动建立的话可以这么简单:

gst-launch filesrc location=/path/to/file.mp3 ! mad ! alsasink

其中的 mad 就是 gstreamer 会 runtime 去 load 的 element ,也就是接下来会深入去讲的主题。如果你的系统缺少了解码 mp3 必要的函式库或 gstreamer 针对 mp3 的插件,那就会播放失败。开源的 mp3 函式库很多,我们就用 mad (mpeg audio decoder)。以 ubuntu 为例,安装必要的函式库很容易:

sudo apt-get install libmad0 gstreamer0.10-plugins-ugly

如此应该就可以顺利听到 mp3 的音乐了。其他必要的 element 像是 audio renderer 通常预设就会安装了。知道了这些工具后我们就可以开始以 mad 为师的 gstreamer 插件学习过程。

首先,我们最好用 gst-inspect 看看 mad 这个插件的一些资料,这些都会是接下来写程式或多或少会用到的。

gst-inspect mad

我们会看到一些对这个插件的描述,pad template 的 capabilities 等等,gstreamer 的文件里有比较清楚的列出哪些 properties 对 capabilities 的描述和对应的意义,此处就不多说。

gst-launch 和 gst-inspect 是开发插件时满重要的两个工具,玩熟练后我们就可以开始实作自己的 mp3 gstreamer 插件。gstreamer 很体贴的在网站上摆了一个插件的 template,我们就从这个 template 开始走下去。

git clone git://anongit.freedesktop.org/gstreamer/gst-template.git 

下载后在作业目录会找到一个 gst-template 的资料夹,然后进到 gst-plugin/src 执行

../tools/make_element mp3dec

这个 tool 会用 mp3dec 为名产生一个 gstreamer plugin 的 template。这两件事情就是在做 gstreamer plugin writer’s guide 的 section 3.1, 3.2。

接著,我们要「立刻」看到自己写的 plugin 被 gst-inspect 找到,这要怎么做呢?

首先,改写 gst-plugin/src/Makefile.am,让他编译我们的程式,用文字编辑器把 gstplugin 这个字串换成 gstmp3dec。接著就像一般我们在编译开源专案一样,藉 autotool 来产生 Makefile,执行 gst-plugin/autogen.sh 。接著到 gst-plugin/src 下 make,就会在 gst-plugin/src/.libs/ 下面看到 libgstmp3dec.so,这个就是我们的 gstreamer 插件。你可以用

GST_PLUGIN_PATH=/path/to/gst-template/gst-plugin/src/.libs/ gst-inspect mp3dec

来检视这个插件的细节,就像之前我们检视 mad 一样,会发现很多资讯在 mad 里面有的,在 mp3dec 这个新生的插件里看不到,那些就是我们要慢慢加上去的功能。

 

2 看见 gstreamer plugin 的第二步       

上一篇我们把一些编译 gstreamer 插件的环境给准备好,也透过 gst-inspect 看到新加入的插件 ( 在上一个例子中是「mp3dec」) 的属性,接著就要亲眼见证它的运作了。

先打开 gstmp3dec.c 找到

g_printf(“I'm plugged, therefore I’m in./n”);

这一行,改一下文字,然后跳出重编,执行

gst-launch filesrc location=/path/to/file.mp3 ! mad ! mp3dec ! alsasink

有没有看到一行你刚刚改的字拚命洗画面,那就是插件运作的明证。接著我们要开始改写这个插件,来让它取代 mad。所以测试方法也很明确,就是要让

gst-launch filesrc location=/path/to/file.mp3 ! mp3dec ! alsasink

这指令可以正确地播出 file.mp3 的内容。这个指令会在接下来的测试过程中不断的被执行。

接著编辑 gstmp3dec.c (这个档案也会不断的修改),寻找 GstStaticPadTemplate ,会找到已经被自动产生的两个 pad:sink_factory 和 src_factory 。还不知道 pad 是什么没关系,先想像它是插件的「开口」就好;上一篇文章我们有提到所谓的 pipeline 的箭头是有方向性的,资料从源头 (档案、网路…等) 读取出来后,从读取的插件开始(即:file-source),到播送的插件出去(即:audio-sink 和 video-sink)。

[转]看见 <wbr>gstreamer <wbr>plugin <wbr>的第二步

透过插件的「开口」,资料才能在插件之间流动,就像滤水器的进水阀和出水阀,控制流进流出的水量、速度等等。不过 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 写起来也比较复杂。

解释完插件和 pad、caps 之间的关系后,我们先透过程式去设定 mp3dec 的属性。为求简单,我们照抄 mad 的属性就好,所以 sink_factory 和 src_factory 会改成如下

 static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/mpeg, / mpegversion=1, / layer=[1,3], / rate={8000,11025,12000,16000,22050,24000,32000,44100,48000},/ channels=[1,2]") ); static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/x-raw-int, / endianness=1234, / signed=true, / width=32, / depth=32, / rate={8000,11025,12000,16000,22050,24000,32000,44100,48000},/ channels=[1,2]") ); 

重编后再用 gst-inspect 检查一下就会发现在 Pad Templates 里所描述 sinkpad 和 srcpad 的属性都更新了,看的出来 mp3dec 接受的输入格式是 mpeg1 audio layer3 的资料流,输出 pcm 。设定这些属性的目的就跟前述一样,让 gstreamer 在自动产生 pipeline 的时候可以按照我们设定的格式找到正确的插件来处理资料。(想像一下滤水器的进入出入阀标示著这个是滤工业用水、那个是滤农业用水、另一个是滤家庭用水,口径多少、每单位吃水量多少…等等等,如此就算滤水器的功能一样,而相对应的口径、水量不符合,gstreamer 也不会接错。)

然而,这边设定的 caps 只是一个样板,告诉上下插件输入和输出资料的格式及相关属性的「范围」,做为建立 pipeline 时参考的依据,当档案开始播放时,真正的资料流的格式、属性要等解码完才知道。换言之,caps 的设定不一定是在 template 里写死就好,有时要另外动态产生运行时对应的 caps 并指派给 pad ( 包括 sinkpad 和 srcpad )。

在处理 sinkpad 和 srcpad 的程式都还没写之前就先设定 caps 其实并没有具体的功能,但我觉得这样解释比较不会搞不清楚或混淆 caps 的目的和重要性。

当 caps 被设定好后,我们再来执行看看前面执行过的指令

gst-launch filesrc location=/path/to/file.mp3 ! mad ! mp3dec ! alsasink

有没有发现结果不一样了?此时音乐不会播,程式直接中断并吐出一行字:

WARNING: erroneous pipeline: could not link mad0 to mp3dec0

原因很简单,就是 gstreamer 发现 mad 的输出阀 (srcpad) 和 mp3dec 的输入阀 (sinkpad) 的 caps 不符合。所以跑都不跑就直接跳掉了。

 

3.推动 gstreamer plugin 的第三步       

前面两篇我们完成了两件很重要的事情,第一是建立了编写插件程式的环境和测试方法,第二是替插件装好了进出水阀 (sinkpad 和 srcpad) 的格式和属性,格式不合的资料进不来,也出不去。接下来我们要开始放水,让资料流进这个插件。

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 调用的部份、处理缓冲伫列等等比较复杂,将另开篇幅说明。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章