Gstreamer Plugin 编写 之 入门

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

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

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

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

gst-launchplaybin uri=file:///path/to/file.mp3

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

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

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



ubuntu 为例,安装必要的函式库很容易:

sudoapt-get install libmad0 gstreamer0.10-plugins-ugly

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

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

gst-inspectmad

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

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_elementmp3dec

这个 tool 会用 mp3dec为名产生一个 gstreamerplugin template。这两件事情就是在做gstreamer plugin writer’s guidesection 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这个新生的插件里看不到,那些就是我们要慢慢加上去的功能。

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

先打开 gstmp3dec.c 找到

g_printf(“I'mplugged, therefore I’m in.\n”);

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

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

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

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

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

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

Gstreamer Plugin 编写 之 入门_第1张图片

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

解释完插件和 padcaps之间的关系后,我们先透过程式去设定mp3dec的属性。为求简单,我们照抄 mad的属性就好,所以 sink_factorysrc_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检查一下就会发现在PadTemplates 里所描述 sinkpad srcpad 的属性都更新了,看的出来mp3dec接受的输入格式是mpeg1 audio layer3 的资料流,输出pcm 。设定这些属性的目的就跟前述一样,让gstreamer在自动产生pipeline的时候可以按照我们设定的格式找到正确的插件来处理资料。(想像一下滤水器的进入出入阀标示著这个是滤工业用水、那个是滤农业用水、另一个是滤家庭用水,口径多少、每单位吃水量多少…等等等,如此就算滤水器的功能一样,而相对应的口径、水量不符合,gstreamer也不会接错。)

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

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

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

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

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

WARNING:erroneous pipeline: could not link mad0 to mp3dec0

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

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

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

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

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

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

Gstreamer 在处理资料流有四个状态:Null,Ready, Pause, Playing按顺序切换。也就是说,刚开始播放一个档案时状态变化是:Null –> Ready –> Pause –>Playing,当播放结束要释放pipeline的顺序就是原路走回去:Playing–> Pause –> Ready –> Null。我们写的这个mp3dec插件是要把mpegaudio decoder libmad包装为gstreamer插件,所以在开始播放档案之前必须先把插件初始化(比如说,设定membervariable的初始值,初始化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),就要释放资源。为了避免当主要的执行续(mainthread)还在运作时,就因为收到「停止」的指令,从PLAYING切进NULL,把资源都给释放掉,所以状态转换要分成两个switch-case来处理。

我们可以试著讨论一下 pipeline如此处理状态切换的理由是什么。想像你手上有一个滤水器,一个水桶的污水和一个干净的水壶


你可能感兴趣的:(gstreamer)