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 插件告個段落時寫個筆記,算是為了交流,也算是幫自己重新確認觀念無誤。

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

這回我想拿 mp3 檔案做例子,用一個比較普遍而且廣泛應用的格式做練習有很多好處,一來沒有影像,就先省去同步的問題,也不會有 cpu / bandwidth 不夠的問題 (因為 video 的 bitrate 比 audio 高出很多);二來測試檔案滿地爬,而且橫跨多種不同參數的壓縮格式,更好的是可以互相參照的播放器也是滿地爬(一不小心就踩到的程度…搭捷運時年輕人幾乎人手一台 player),抓蟲或對照功能時很好用。

gstreamer 提供了一個 command line 建立 pipeline 的工具:gst-launch。給不同的參數可以自動或手動的方式去播放一個多媒體檔,這個工具說方便很方便,說不方便也的確有點麻煩。方便是一個指令就可以叫它開始播檔案,省去圖型化介面的慢和滑老鼠的動作;不方便是因為它除了 play 以外沒有別的 navigation command,不像 mplayer 還有給 hotkey 快轉 (快轉對於看謎片來說是很重要的呀!!)。至於所謂的 pipeline,長得就像這樣

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

箭頭和方塊組成的結構就稱為 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)。

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

透過插件的「開口」,資料才能在插件之間流動,就像濾水器的進水閥和出水閥,控制流進流出的水量、速度等等。不過 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 調用的部份、處理緩衝佇列等等比較複雜,將另開篇幅說明。

 

 

你可能感兴趣的:(plugin,gsteamer)