一、基本概念
GStreamer 作為 GNOME 桌面環境推薦的流媒體應用框架,采用了基於插件(plugin)和管道(pipeline)的體系結構,框架中的所有的功能模塊都被實現成可以插拔的組件(component), 並且在需要的時候能夠很方便地安裝到任意一個管道上,由於所有插件都通過管道機制進行統一的數據交換,因此很容易利用已有的各種插件“組裝”出一個功能完善的多媒體應用程序。
1.1 元件處理
對於需要應用 GStreamer 框架的程序員來講,GstElement 是一個必須理解的概念,因為它是組成管道的基本構件,也是框架中所有可用組件的基礎,這也難怪 GStreamer 框架中的大部分函數都會涉及到對 GstElement 對象的操作。從 GStreamer 自身的觀點來看,GstElement 可以描述為一個具有特定屬性的黑盒子,它通過連接點(link point)與外界進行交互,向框架中的其余部分表征自己的特性或者功能。
按照各自功能上的差異,GStreamer 又將 GstElement 細分成如下幾類:
- Source Element 數據源元件 只有輸出端,它僅能用來產生供管道消費的數據,而不能對數據做任何處理。一個典型的數據源元件的例子是音頻捕獲單元,它負責從聲卡讀取原始的音頻數據,然后作為數據源提供給其它模塊使用。
- Filter Element 過濾器元件 既有輸入端又有輸出端,它從輸入端獲得相應的數據,並在經過特殊處理之后傳遞給輸出端。一個典型的過濾器元件的例子是音頻編碼單元,它首先從外界獲得音頻數據,然后根據特定的壓縮算法對其進行編碼,最后再將編碼后的結果提供給其它模塊使用。
- Sink Element 接收器元件 只有輸入端,它僅具有消費數據的能力,是整條媒體管道的終端。一個典型的接收器元件的例子是音頻回放單元,它負責將接收到的數據寫到聲卡上,通常這也是音頻處理過程中的最后一個環節。
圖1將有助於你更好地理解數據源元件、過濾器元件和接收器元件三者的區別,同時也不難看出它們是如何相互配合形成管道的:
圖1
需要注意的是,過濾器元件的具體形式是非常靈活的,GStreamer並沒有嚴格規定輸入端和輸出端的數目,事實上它們都可以是一個或者多個。圖2是一個AVI分離器的基本結構,它能夠將輸入數據分離成單獨的音頻信息和視頻信息,用於實現該功能的過濾器元件很明顯只具有一個輸入端,但卻需要有兩個輸出端。
圖2
要想在應用程序中創建GstElement對象,唯一的辦法是借助於工廠對象GstElementFactory。由於GStreamer框架提供了多種類型的GstElement對象,因此對應地提供了多種類型的GstElementFactory對象,它們是通過特定的工廠名稱來進行區分的。例如,下面的代碼通過gst_element_factory_find()函數獲得了一個名為mad的工廠對象,它之后可以用來創建與之對應的MP3解碼器元件:
GstElementFactory *factory; factory = gst_element_factory_find ("mad");
成功獲得工廠對象之后,接下來就可以通過gst_element_factory_create()函數來創建特定的GstElement對象了,該函數在調用時有兩個參數,分別是需要用到的工廠對象,以及即將創建的元件名稱。元件名稱可以用查詢的辦法獲得,也可以通過傳入空指針(NULL)來生成工廠對象的默認元件。下面的代碼示范了如何利用已經獲得的工廠對象,來創建名為decoder的MP3解碼器元件:
GstElement *element; element = gst_element_factory_create (factory, "decoder");
當創建的GstElement不再使用的時候,還必須調用gst_element_unref()函數釋放其占用的內存資源:
gst_element_unref (element);
GStreamer使用了與GObject相同的機制來對屬性(property)進行管理,包括查詢(query)、設置(set)和讀取(get)等。所有的GstElement對象都需要從其父對象GstObject那里繼承名稱(name)這一最基本的屬性,這是因為像gst_element_factory_make()和gst_element_factory_create()這樣的函數在創建工廠對象和元件對象時都會用到名稱屬性,通過調用gst_object_set_name()和gst_object_get_name()函數可以設置和讀取GstElement對象的名稱屬性。
1.2 襯墊處理
襯墊(pad)是GStreamer框架引入的另外一個基本概念,它指的是元件(element)與外界的連接通道,對於框架中的某個特定元件來說,其能夠處理的媒體類型正是通過襯墊暴露給其它元件的。成功創建GstElement對象之后,可以通過gst_element_get_pad()獲得該元件的指定襯墊。例如,下面的代碼將返回element元件中名為src的襯墊:
GstPad *srcpad; srcpad = gst_element_get_pad (element, "src");
如果需要的話也可以通過gst_element_get_pad_list()函數,來查詢指定元件中的所有襯墊。例如,下面的代碼將輸出element元件中所有襯墊的名稱:
GList *pads; pads = gst_element_get_pad_list (element); while (pads) { GstPad *pad = GST_PAD (pads->data); g_print ("pad name is: %s\n", gst_pad_get_name (pad)); pads = g_list_next (pads); }
與元件一樣,襯墊的名稱也能夠動態設置或者讀取,這是通過調用gst_pad_get_name ()和gst_pad_set_name()函數來完成的。所有元件的襯墊都可以細分成輸入襯墊和輸出襯墊兩種,其中輸入襯墊只能接收數據但不能產生數據,而輸出襯墊則正好相反,只能產生數據但不能接收數據,利用函數gst_pad_get_direction()可以獲得指定襯墊的類型。GStreamer框架中的所有襯墊都必然依附於某個元件之上,調用gst_pad_get_parent()可以獲得指定襯墊所屬的元件,該函數的返回值是一個指向GstElement的指針。 襯墊從某種程度上可以看成是元件的代言人,因為它要負責向外界描述該元件所具有的能力。GStreamer框架提供了統一的機制來讓襯墊描述元件所具有的能力(capability),這是借助數據結構_GstCaps來實現的:
struct _GstCaps { gchar *name; /* the name of this caps */ guint16 id; /* type id (major type) */ guint refcount; /* caps are refcounted */ GstProps *properties; /* properties for this capability */ GstCaps *next; /* caps can be chained together */ };
以下是對mad元件的能力描述,不難看出該元件中實際包含sink和src兩個襯墊,並且每個襯墊都帶有特定的功能信息。名為sink的襯墊是mad元件的輸入端,它能夠接受MIME類型為audio/mp3的媒體數據,此外還具有layer、bitrate和framed三種屬性。名為src的襯墊是mad元件的輸出端,它負責產生MIME類型為audio/raw媒體數據,此外還具有format、depth、rate和channels等多種屬性。
Pads: SINK template: ’sink’ Availability: Always Capabilities: ’mad_sink’: MIME type: ’audio/mp3’: SRC template: ’src’ Availability: Always Capabilities: ’mad_src’: MIME type: ’audio/raw’: format: String: int endianness: Integer: 1234 width: Integer: 16 depth: Integer: 16 channels: Integer range: 1 - 2 law: Integer: 0 signed: Boolean: TRUE rate: Integer range: 11025 - 48000
准確地說,GStreamer框架中的每個襯墊都可能對應於多個能力描述,它們能夠通過函數gst_pad_get_caps()來獲得。例如,下面的代碼將輸出pad襯墊中所有能力描述的名稱及其MIME類型:
GstCaps *caps; caps = gst_pad_get_caps (pad); g_print ("pad name is: %s\n", gst_pad_get_name (pad)); while (caps) { g_print (" Capability name is %s, MIME type is %s\n", gst_caps_get_name (cap), gst_caps_get_mime (cap)); caps = caps->next; }
1.3 箱櫃
箱櫃(bin)是GStreamer框架中的容器元件,它通常被用來容納其它的元件對象,但由於其自身也是一個GstElement對象,因此實際上也能夠被用來容納其它的箱櫃對象。利用箱櫃可以將需要處理的多個元件組合成一個邏輯元件,由於不再需要對箱櫃中的元件逐個進行操作,因此能夠很容易地利用它來構造更加復雜的管道。在GStreamer框架中使用箱櫃還有另外一個優點,那就是它會試着對數據流進行優化,這對於多媒體應用來講是很具吸引力的。
圖3描述了箱櫃在GStreamer框架中的典型結構:
圖3
在GStreamer應用程序中使用的箱櫃主要有兩種類型:
- GstPipeline 管道是最常用到的容器,對於一個GStreamer應用程序來講,其頂層箱櫃必須是一條管道。
- GstThread 線程的作用在於能夠提供同步處理能力,如果GStreamer應用程序需要進行嚴格的音視頻同步,一般都需要用到這種類型的箱櫃。
GStreamer框架提供了兩種方法來創建箱櫃:一種是借助工廠方法,另一種則是使用特定的函數。下面的代碼示范了如何使用工廠方法創建線程對象,以及如何使用特定函數來創建管道對象:
GstElement *thread, *pipeline; // 創建線程對象,同時為其指定唯一的名稱。 thread = gst_element_factory_make ("thread", NULL); // 根據給出的名稱,創建一個特定的管道對象。 pipeline = gst_pipeline_new ("pipeline_name");
箱櫃成功創建之后,就可以調用gst_bin_add()函數將已經存在的元件添加到其中來了:
GstElement *element; GstElement *bin; bin = gst_bin_new ("bin_name"); element = gst_element_factory_make ("mpg123", "decoder"); gst_bin_add (GST_BIN (bin), element);
而要從箱櫃中找到特定的元件也很容易,可以借助gst_bin_get_by_name()函數實現:
GstElement *element; element = gst_bin_get_by_name (GST_BIN (bin), "decoder");
由於GStreamer框架中的一個箱櫃能夠添加到另一個箱櫃之中,因此有可能會出現箱櫃嵌套的情況,gst_bin_get_by_name()函數在查找元件時會對嵌套的箱櫃作遞歸查找。元件有添加到箱櫃之中以后,在需要的時候還可以從中移出,這是通過調用gst_bin_remove()函數來完成的:
GstElement *element; gst_bin_remove (GST_BIN (bin), element);
如果仔細研究一下圖3中描述的箱櫃,會發現它沒有屬於自己的輸入襯墊和輸出襯墊,因此顯然是無法作為一個邏輯整體與其它元件交互的。為了解決這一問題,GStreamer引入了精靈襯墊(ghost pad)的概念,它是從箱櫃里面所有元件的襯墊中推舉出來的,通常來講會同時選出輸入襯墊和輸出襯墊,如圖4所示:
圖4
具有精靈襯墊的箱櫃在行為上與元件是完全相同的,所有元件具有的屬性它都具有,所有針對元件能夠進行的操作也同樣能夠針對箱櫃進行,因此在GStreamer應用程序中能夠像使用元件一樣使用這類箱櫃。下面的代碼示范了如何為箱櫃添加一個精靈襯墊:
GstElement *bin; GstElement *element; element = gst_element_factory_create ("mad", "decoder"); bin = gst_bin_new ("bin_name"); gst_bin_add (GST_BIN (bin), element); gst_element_add_ghost_pad (bin, gst_element_get_pad (element, "sink"), "sink");
二、元件連接
在引入了元件和襯墊的概念之后,GStreamer對多媒體數據的處理過程就變得非常清晰了:通過將不同元件的襯墊依次連接起來構成一條媒體處理管道,使數據在流經管道的過程能夠被各個元件正常處理,最終實現特定的多媒體功能。
圖1就描述了一條很簡單的管道,它由三個基本元件構成:數據源元件只負責產生數據,它的輸出襯墊與過濾器元件的輸入襯墊相連;過濾器元件負責從自己的輸入襯墊中獲取數據,並在經過特定的處理之后,將結果通過輸出襯墊傳給與之相連的接收器元件;接收器元件只負責接收數據,它的輸入襯墊與過濾器元件的輸出襯墊相連,負責對最終結果進行相應的處理。
GStreamer框架中的元件是通過各自的襯墊連接起來的,下面的代碼示范了如何將兩個元件通過襯墊連接起來,以及如何在需要的時候斷開它們之間的連接:
GstPad *srcpad, *sinkpad; srcpad = gst_element_get_pad (element1, "src"); sinpad = gst_element_get_pad (element2, "sink"); // 連接 gst_pad_link (srcpad, sinkpad); // 斷開 gst_pad_unlink (srcpad, sinkpad);
如果需要建立起連接的元件都只有一個輸入襯墊和一個輸出襯墊,那么更簡單的做法是調用gst_element_link()函數直接在它們之間建立起連接,或者調用gst_element_unlink()函數斷開它們之間的連接:
// 連接 gst_element_link (element1, element2); // 斷開 gst_element_unlink (element1, element2);
三、元件狀態
當GStreamer框架中的元件通過管道連接好之后,它們就開始了各自的處理流程,期間一般會經歷多次狀態切換,其中每個元件在特定時刻將處於如下四種狀態之一:
- NULL 這是所有元件的默認狀態,表明它剛剛創建,還沒有開始做任何事情。
- READY 表明元件已經做好准備,隨時可以開始處理流程。
- PAUSED 表明元件因某種原因暫時停止處理數據。
- PLAYING 表明元件正在進行數據處理。
所有的元件都從NULL狀態開始,依次經歷NULL、READY、PAUSED、PLAYING等狀態間的轉換。元件當前所處的狀態可以通過調用gst_element_set_state()函數進行切換:
GstElement *bin; /* 創建元件,並將其連接成箱櫃bin */ gst_element_set_state (bin, GST_STATE_PLAYING);
默認情況下,管道及其包含的所有元件在創建之后將處於NULL狀態,此時它們不會進行任何操作。當管道使用完畢之后,不要忘記重新將管道的狀態切換回NULL狀態,讓其中包含的所有元件能夠有機會釋放它們正在占用的資源。
管道真正的處理流程是從第一次將其切換到READY狀態時開始的,此時管道及其包含的所有元件將做好相應的初始化工作,來為即將執行的數據處理過程做好准備。對於一個典型的元件來講,處於READY狀態時需要執行的操作包括打開媒體文件和音頻設備等,或者試圖與位於遠端的媒體服務器建立起連接。
處於READY狀態的管道一旦切換到PLAYING狀態,需要處理的多媒體數據就開始在整個管道中流動,並依次被管道中包含的各個元件進行處理,從而最終實現管道預先定義好的某種多媒體功能。GStreamer框架也允許將管道直接從NULL狀態切換到PLAYING狀態,而不必經過中間的READY狀態。
正處於播放狀態的管道能夠隨時切換到PAUSED狀態,暫時停止管道中所有數據的流動,並能夠在需要的時候再次切換回PLAYING狀態。如果需要插入或者更改管道中的某個元件,必須先將其切換到PAUSED或者NULL狀態,元件在處於PAUSED狀態時並不會釋放其占用的資源。
四、實現MP3播放器
在理解了一些基本概念和處理流程之后,下面來看看如何利用GStreamer框架提供的組件,來實現一個簡單的MP3播放器。在圖1中描述的結構能夠很容易地映射成MP3播放器,其中數據源元件負責從磁盤上讀取數據,過濾器元件負責對數據進行解碼,而接受器元件則負責將解碼后的數據寫入聲卡。
與其它眾多GNOME項目一樣,GStreamer也是用C語言實現的。如果想要在程序中應用GStreamer提供的各種功能,首先必須在主函數中調用gst_init()來完成相應的初始化工作,以便將用戶從命令行輸入的參數傳遞給GStreamer函數庫。一個典型的GStreamer應用程序的初始化如下所示:
#include <gst/gst.h> int main (int argc, char *argv[]) { gst_init (&argc, &argv); /* ... */ }
接下去需要創建三個元件並連接成管道,由於所有GStreamer元件都具有相同的基類GstElement,因此能夠采用如下方式進行定義:
GstElement *pipeline, *filesrc, *decoder, *audiosink;
管道在GStreamer框架中是用來容納和管理元件的,下面的代碼將創建一條名為pipeline的新管道:
/* 創建用來容納元件的新管道 */ pipeline = gst_pipeline_new ("pipeline");
數據源元件負責從磁盤文件中讀取數據,它具有名為location的屬性,用來指明文件在磁盤上的位置。使用標准的GObject屬性機制可以為元件設置相應的屬性:
/* 創建數據源元件 */ filesrc = gst_element_factory_make ("filesrc", "disk_source"); g_object_set (G_OBJECT (filesrc), "location", argv[1], NULL);
過濾器元件負責完成對MP3格式的數據進行解碼,最簡單的辦法是安裝mad這一插件,借助它來完成相應的解碼工作:
/* 創建過濾器元件 */ decoder = gst_element_factory_make ("mad", "decoder");
接收器元件負責將解碼后的數據利用聲卡播放出來:
/* 創建接收器元件 */ audiosink = gst_element_factory_make ("audiosink", "play_audio");
已經創建好的三個元件需要全部添加到管道中,並按順序連接起來:
/* 添加元件到管道中 */ gst_bin_add_many (GST_BIN (pipeline), filesrc, decoder, audiosink, NULL); /* 通過襯墊連接元件 */ gst_element_link_many (filesrc, decoder, audiosink, NULL);
所有准備工作都做好之后,就可以通過將管道的狀態切換到PLAYING狀態,來啟動整個管道的數據處理流程:
/* 啟動管道 */ gst_element_set_state (pipeline, GST_STATE_PLAYING);
由於沒有用到線程,因此必須通過不斷調用gst_bin_iterate()函數的辦法,來判斷管道的處理過程會在何時結束:
while (gst_bin_iterate (GST_BIN (pipeline)));
只要管道內還會繼續有新的事件產生,gst_bin_iterate()函數就會一直返回TRUE,只有當整個處理過程都結束的時候,該函數才會返回FALSE,此時就該終止管道並釋放占用的資源了:
/* 終止管道 */ gst_element_set_state (pipeline, GST_STATE_NULL); /* 釋放資源 */ gst_object_unref (GST_OBJECT (pipeline));
用GStreamer實現的MP3播放器的源代碼如下所示:
#include <gst/gst.h> int main (int argc, char *argv[]) { GstElement *pipeline, *filesrc, *decoder, *audiosink; gst_init(&argc, &argv); if (argc != 2) { g_print ("usage: %s <mp3 filename>\n", argv[0]); exit (-1); } /* 創建一條新的管道 */ pipeline = gst_pipeline_new ("pipeline"); /* 生成用於讀取硬盤數據的元件 */ filesrc = gst_element_factory_make ("filesrc", "disk_source"); g_object_set (G_OBJECT (filesrc), "location", argv[1], NULL); /* 創建解碼器元件 */ decoder = gst_element_factory_make ("mad", "decoder"); /* 創建音頻回放元件 */ audiosink = gst_element_factory_make ("osssink", "play_audio"); /* 將生成的元件添加到管道中 */ gst_bin_add_many (GST_BIN (pipeline), filesrc, decoder, audiosink, NULL); /* 連接各個元件 */ gst_element_link_many (filesrc, decoder, audiosink, NULL); /* 開始播放 */ gst_element_set_state (pipeline, GST_STATE_PLAYING); while (gst_bin_iterate (GST_BIN (pipeline))); /* 停止管道處理流程 */ gst_element_set_state (pipeline, GST_STATE_NULL); /* 釋放占用的資源 */ gst_object_unref (GST_OBJECT (pipeline)); exit (0); }
五、小結
隨着 GNOME 桌面環境的不斷普及,GStreamer 作為一個強大的多媒體應用開發框架,已經開始受到越來越多人的關注。Gstreamer在設計時采用了非常靈活的體系結構,並且提供了許多預定義的媒體處理模塊,因此能夠極大簡化在Linux下開發多媒體應用的難度。