Gstreamer 工具使用(二)

前面兩篇我們完成了兩件很重要的事情,第一是建立了編寫插件程式的環境和測試方法,第二是替插件裝好了進出水閥 (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 調用的部份、處理緩衝佇列等等比較複雜,將另開篇幅說明

你可能感兴趣的:(Gstreamer 工具使用(二))