詳細的GStreamer開發教程

詳細的GStreamer開發教程



本文主要參考GStreamer官方Tutorials:gstreamer Tutorials

1. 什麼是GStreamer?

Gstreamer是一個用於開發流媒體應用的開源框架,採用了基於插件(plugin)和管道(pipeline)的體系結構,框架中的所有的功能模塊都被實現成可以插拔的組件(component), 並且能夠很方便地安裝到任意一個管道上。由於所有插件都通過管道機制進行統一的數據交換,因此很容易利用已有的各種插件“組裝”出一個功能完善的流媒體應用程序。

由於deepstream是基於gstreamer的,所以要想在deepstream上做拓展,需要對gstreamer有一定的認識。

2. GStreamer架構

GStreamer的核心功能是爲插件,數據流和媒體類型處理/協商提供框架。 下圖是對基於Gstreamer框架的應用的簡單分層:
在這裏插入圖片描述

2.1 Media Applications

最上面一層爲應用,比如gstreamer自帶的一些工具(gst-launch,gst-inspect等),以及基於gstreamer封裝的庫(gst-player,gst-rtsp-server,gst-editing-services等)根據不同場景實現的應用。

2.2 Core Framework

中間一層爲Core Framework,主要提供:

  • 上層應用所需接口
  • Plugin的框架
  • Pipline的框架
  • 數據在各個Element間的傳輸及處理機制
  • 多個媒體流(Streaming)間的同步(比如音視頻同步)
  • 其他各種所需的工具庫

2.3 Plugins

最下層爲各種插件,實現具體的數據處理及音視頻輸出,應用不需要關注插件的細節,會由Core Framework層負責插件的加載及管理。主要分類爲:

  • Protocols:負責各種協議的處理,file,http,rtsp等。
  • Sources:負責數據源的處理,alsa,v4l2,tcp/udp等。
  • Formats:負責媒體容器的處理,avi,mp4,ogg等。
  • Codecs:負責媒體的編解碼,mp3,vorbis等。
  • Filters:負責媒體流的處理,converters,mixers,effects等。
  • Sinks:負責媒體流輸出到指定設備或目的地,alsa,xvideo,tcp/udp等。

3. GStreamer組件

3.1 Element

對於需要應用 GStreamer 框架的程序員來講,GstElement 是一個必須理解的概念,因爲它是組成管道的基本構件,也是框架中所有可用組件的基礎。因此,GStreamer 框架中的大部分函數都會涉及到對 GstElement 對象的操作。

從 GStreamer 自身的觀點來看,GstElement 可以描述爲一個具有特定屬性的黑盒子,它通過連接點(link point)與外界進行交互,向框架中的其餘部分表徵自己的特性或者功能。 一個Element實現了一個功能(如讀取文件,解碼,輸出等),一個程序需要創建多個element,並按順序將其串連起來,構成一個完整的pipeline。

按照各自功能上的差異,GstElement 可以細分成如下幾類:

  • Source Element 數據源元件 只有輸出端,它僅能用來產生供管道消費的數據,而不能對數據做任何處理。一個典型的數據源元件的例子是音頻捕獲單元,它負責從聲卡讀取原始的音頻數據,然後作爲數據源提供給其它模塊使用。

在這裏插入圖片描述

  • Filter Element 過濾器元件 既有輸入端又有輸出端,它從輸入端獲得相應的數據,並在經過特殊處理之後傳遞給輸出端。 一個典型的過濾器元件的例子是音頻編碼單元,它首先從外界獲得音頻數據,然後根據特定的壓縮算法對其進行編碼,最後再將編碼後的結果提供給其它模塊使用。

在這裏插入圖片描述或者 img

  • Sink Element 接收器元件 只有輸入端,它僅具有消費數據的能力,是整條媒體管道的終端。一個典型的接收器元件的例子是音頻回放單元,它負責將接收到的數據寫到聲卡上,通常這也是音頻處理過程中的最後一個環節。

在這裏插入圖片描述

下圖將有助於你更好地理解數據源元件、過濾器元件和接收器元件三者的區別,同時也不難看出它們是如何相互配合形成管道的:
在這裏插入圖片描述

創建一個 GstElement

要想在應用程序中創建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");

此外,創建元件的最簡單方法是使用 gst_element_factory_make()gst_element_factory_make實際上是兩個功能組合的簡寫,此函數爲新創建的元件採用工廠名稱和元件名稱。

element = gst_element_factory_make ("mad", "decoder");
if (!element) {
    g_print ("Failed to create element of type 'mad'\n");
    return -1;
}

當創建的GstElement不再使用的時候,還必須調用gst_element_unref()函數釋放其佔用的內存資源,gst_element_unref()會將元件的引用計數減少1。元件在創建時的引用計數爲1。當refcount減少到0時,元件將被完全銷燬。

gst_element_unref (element);

GStreamer使用了與GObject相同的機制來對屬性(property)進行管理,包括查詢(query)、設置(set)和讀取(get)等。所有的GstElement對象都從其父對象GstObject那裏繼承了名稱(name)這一最基本的屬性,這是因爲像gst_element_factory_make()gst_element_factory_create()這樣的函數在創建元件對象時都會用到名稱name屬性,通過調用gst_object_set_name()和gst_object_get_name()函數可以設置和讀取GstElement對象的名稱屬性。

一個完整地創建GstElement的例子如下所示。

#include <gst/gst.h>

int main (int argc, char *argv[])
{
  GstElementFactory *factory;
  GstElement * element;

  /* init GStreamer */
  gst_init (&argc, &argv);

  /* create element, method #2 */
  factory = gst_element_factory_find ("fakesrc");
  if (!factory) {
    g_print ("Failed to find factory of type 'fakesrc'\n");
    return -1;
  }
  element = gst_element_factory_create (factory, "source");
  if (!element) {
    g_print ("Failed to create element, even though its factory exists!\n");
    return -1;
  } 
  
  /* get name */
  g_object_get (G_OBJECT (element), "name", &name, NULL);
  g_print ("The name of the element is '%s'.\n", name);
  g_free (name);
  
  gst_object_unref (GST_OBJECT (element));
  gst_object_unref (GST_OBJECT (factory));

  return 0;
}

3.2 箱櫃(bin)

箱櫃(bin)是GStreamer框架中的容器元件,它通常被用來容納其它的元件對象,但由於其自身也是一個GstElement對象,因此實際上也能夠被用來容納其它的箱櫃對象。利用bin可以將需要處理的多個元件組合成一個邏輯元件,你不再需要對箱櫃中的元件逐個進行操作,而只需處理一個bin元件即可。

例如:你有很多個元件用來解碼視頻並對其使用一些效果,要使事情變得簡單一些, 你可以把這些元件放進一個bin(就像一個容器)中,以後你就可以使用bin來引用這些元件了。 這樣其實bin變成了一個元件,如你的管道是 a ! b ! c ! d ,你可以把他們放進 mybin,這樣當你使用mybin時其實是引用了 a ! b ! c ! d

下圖描述了箱櫃在GStreamer框架中的典型結構:
在這裏插入圖片描述

在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 ("mad", "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);

請注意,您添加元件的箱櫃將獲得該元件的所有權,如果銷燬箱櫃,則該元件將被取消引用,如果從箱櫃容器中刪除元件,則將自動取消引用該元件。

下面是創建pipeline Bin容器的代碼示例:

#include <gst/gst.h>

int main (int argc, char *argv[])
{
  GstElement *bin, *pipeline, *source, *sink;

  /* init */
  gst_init (&argc, &argv);

  /* create */
  pipeline = gst_pipeline_new ("my_pipeline");
  bin = gst_bin_new ("my_bin");
  source = gst_element_factory_make ("fakesrc", "source");
  sink = gst_element_factory_make ("fakesink", "sink");

  /* First add the elements to the bin */
  gst_bin_add_many (GST_BIN (bin), source, sink, NULL);
  /* add the bin to the pipeline */
  gst_bin_add (GST_BIN (pipeline), bin);

  /* link the elements */
  gst_element_link (source, sink);
  [...]
}
  

元件的狀態

當GStreamer框架中的元件通過管道連接好之後,它們就開始了各自的處理流程,期間一般會經歷多次狀態切換,其中每個元件在特定時刻將處於如下四種狀態之一:

  • GST_STATE_NULL:這是默認狀態。在這種狀態下沒有分配資源,因此,轉換到此狀態將釋放所有資源。當元件的引用計數達到0並被釋放時,該元件必須處於此狀態。
  • GST_STATE_READY:在就緒狀態下,元件已分配了其所有全局資源,即可以保留在流中的資源。你可以考慮打開設備,分配緩衝區等。但是在這種狀態下不會打開流,因此流位置自動爲零。如果先前已打開流,則應在此狀態下將其關閉,並應重置位置,屬性等。
  • GST_STATE_PAUSED:在此狀態下,元件已打開流,但未對其進行處理。此時元件可以修改流的位置,讀取和處理數據,狀態一旦更改爲PLAYING,即可開始播放。總之,PAUSED與PLAYING相同,只是PAUSED沒有運行時鐘。
  • GST_STATE_PLAYING:在該PLAYING狀態下,與該PAUSED狀態下完全相同,只是時鐘現在運行。

所有的元件都從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狀態時並不會釋放其佔用的資源。

3.3 襯墊(Pad)

襯墊(pad)是GStreamer框架引入的另外一個基本概念,它指的是元件(element)與外界的連接通道, 襯墊pad是元件的輸入和輸出,它們用於協商GStreamer中元件之間的鏈接和數據流。可以將pad視爲元件上的“插頭”或“端口”,與其他元件建立鏈接後,數據可以通過pad流入或流出其他元件。

絕大多數元件有一個輸入pad(sink)和一個輸出pad(src)。 因此,我們上面的管道看起來是這樣的:

[src] ! [sink src]  ! [sink]

最左邊的元件只有一個src pad用來提供數據,接下來的元件接收信息並做一些處理後,傳給下一個元件,因此他們有sink和src pad,最後一個元件sink pad只接收數據。

下面看一下如何獲取元件的襯墊pad。

成功創建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()函數來完成的。

所有元件的襯墊都可以細分成輸入襯墊(sink)和輸出襯墊(src)兩種,其中輸入襯墊只能接收數據但不能產生數據,而輸出襯墊則正好相反,只能產生數據但不能接收數據。GStreamer框架中的所有襯墊都必然依附於某個元件之上,調用gst_pad_get_parent()可以獲得指定襯墊所屬的元件,該函數的返回值是一個指向GstElement的指針。

元件鏈接(Pad link)

在引入了元件和襯墊的概念之後,GStreamer對多媒體數據的處理過程就變得非常清晰了:通過將不同元件的襯墊依次連接起來構成一條媒體處理管道,使數據在流經管道的過程能夠被各個元件正常處理,最終實現特定的多媒體功能。

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);

Pad Capability

襯墊從某種程度上可以看成是元件的代言人,因爲它要負責向外界描述該元件所具有的能力。GStreamer框架提供了統一的機制來讓襯墊描述元件所具有的能力(Capability)。Capabilities簡稱caps,它描繪了兩個pad之間可以支持的數據類型,具體是通過數據結構_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 */
};

當你使用 gst-inspect-1.0 命令查看一個元件的詳細信息時,就會列出該元件的pad信息。

以下是對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;
}

Pad Capability for filtering

如果我想讓兩個元件之間的數據流變爲某種媒體類型video/x-raw,並限制幀率等狀態,可以通過Caps filter實現,最簡單的方法是使用函數gst_caps_new_simple()

static gboolean
link_elements_with_filter (GstElement *element1, GstElement *element2)
{
  gboolean link_ok;
  GstCaps *caps;

  caps = gst_caps_new_simple ("video/x-raw",
          "format", G_TYPE_STRING, "I420",
          "width", G_TYPE_INT, 384,
          "height", G_TYPE_INT, 288,
          "framerate", GST_TYPE_FRACTION, 25, 1,
          NULL);

  link_ok = gst_element_link_filtered (element1, element2, caps);
  gst_caps_unref (caps);

  if (!link_ok) {
    g_warning ("Failed to link element1 and element2!");
  }

  return link_ok;
}

這將迫使兩個元件之間的數據流變爲某種視頻格式(寬度,高度和幀速率等)。如果在所涉及元件的上下文中無法實現鏈接,則鏈接將失敗。

注意:使用gst_element_link_filtered()它時,它將自動爲您創建一個capsfilter元件,並將其插入到您要連接的兩個元件之間的容器或管道中,如果您要斷開這些元件的連接,就必須從capsfilter斷開這兩個元件連接。

精靈襯墊(ghost pad)

如果仔細研究一下箱櫃,會發現bin沒有屬於自己的輸入襯墊和輸出襯墊,因此顯然是無法作爲一個邏輯整體與其它元件交互的。爲了解決這一問題,GStreamer引入了精靈襯墊(ghost pad)的概念, ghost pad是與bin中某些元件的pad相互連接, 它與UNIX文件系統中的符號鏈接類似,如下圖所示:在bin上使用ghost pad,鏈接到Element1的sink,這樣可以通過操作bin上的ghost pad操作Element1 sink pad。

在這裏插入圖片描述

具有精靈襯墊(ghost pad)的箱櫃在行爲上與元件是完全相同的,所有元件具有的屬性它都具有,所有針對元件能夠進行的操作也同樣能夠針對箱櫃進行,因此在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");

動態襯墊( Dynamic pads )

創建元件時,某些元件可能沒有包含全部pad。例如,創建ogg demuxer元件時會發生src pad不會立即創建的情況。這是因爲此時demuxer並不知道輸入流經過demux後需要輸出什麼類型的流?輸出幾路流?因此demuxer的src pad不會立即創建。動態襯墊會根據輸入文件的類型,自動創建襯墊pad。

當demuxer元件檢測到ogg流時,它會讀取ogg流並根據ogg流的信息爲每個流動態創建pad。當ogg流結束時,demuxer元件將會刪除動態src pad。

我們使用gst-inspect oggdemux命令來查看oggdemux的信息,結果顯示該元件只有一個襯墊:“一個sink墊”。其他襯墊都是“ dormant(休眠)”狀態。
你可以在oggdemux的src pad caps中看到此內容,其中有一個“Exists: Sometimes”的屬性,表示該pad是並不總是存在的。

#include <gst/gst.h>

static void cb_new_pad (GstElement *element, GstPad *pad, gpointer data)
{
  gchar *name;
  name = gst_pad_get_name (pad);
  g_print ("A new pad %s was created\n", name);
  g_free (name);
  /* here, you would setup a new pad link for the newly created pad */
[..]
}

int main (int argc, char *argv[])
{
  GstElement *pipeline, *source, *demux;
  GMainLoop *loop;
    
  /* init */
  gst_init (&argc, &argv);
    
  /* create elements */
  pipeline = gst_pipeline_new ("my_pipeline");
  source = gst_element_factory_make ("filesrc", "source");
  g_object_set (source, "location", argv[1], NULL);
  demux = gst_element_factory_make ("oggdemux", "demuxer");

  /* you would normally check that the elements were created properly */
  /* put together a pipeline */
  gst_bin_add_many (GST_BIN (pipeline), source, demux, NULL);
  gst_element_link_pads (source, "src", demux, "sink");

  /* listen for newly created pads */
  g_signal_connect (demux, "pad-added", G_CALLBACK (cb_new_pad), NULL);

  /* start the pipeline */
  gst_element_set_state (GST_ELEMENT (pipeline), GST_STATE_PLAYING);
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

[..]

}

這裏通過g_signal_connect監聽"pad-added"信號,並使用G_CALLBACK (cb_new_pad)回調處理監聽到的信號。最後不要忘記使用gst_element_set_state ()gst_element_sync_state_with_parent () 將新添加的元素的狀態設置爲管道的目標狀態。

3.4 總線(Bus)

總線是一個簡單的系統,負責將消息從管道傳遞到應用程序。默認情況下,每個管道都包含一個總線,因此應用程序不需要創建總線或任何東西。應用程序唯一要做的就是在總線上設置消息處理程序,這類似於對對象的信號處理程序。當mainloop運行時,將定期檢查總線上是否有新消息,並且在有任何消息可用時將調用回調。

如何使用Bus?

有兩種總線使用方式:

  • 運行GLib/Gtk+ main loop,並將某種watch連接到總線。這樣,GLib main loop將檢查總線上是否有新消息,並在有消息時觸發信號,此時可以使用回調函數進行處理。通常使用gst_bus_add_watch ()gst_bus_add_signal_watch ()來監聽Bus,並將消息處理程序附加到管道的總線上,每當管道向總線發出消息時,都將調用此處理程序。
  • 自己檢查總線上的消息。可以使用gst_bus_peek ()和/或完成此操作 gst_bus_poll ()
#include <gst/gst.h>

static GMainLoop *loop;

static gboolean
my_bus_callback (GstBus * bus, GstMessage * message, gpointer data)
{
  g_print ("Got %s message\n", GST_MESSAGE_TYPE_NAME (message));

  switch (GST_MESSAGE_TYPE (message)) {
    case GST_MESSAGE_ERROR:{
      GError *err;
      gchar *debug;

      gst_message_parse_error (message, &err, &debug);
      g_print ("Error: %s\n", err->message);
      g_error_free (err);
      g_free (debug);

      g_main_loop_quit (loop);
      break;
    }
    case GST_MESSAGE_EOS:
      /* end-of-stream */
      g_main_loop_quit (loop);
      break;
    default:
      /* unhandled message */
      break;
  }

  /* we want to be notified again the next time there is a message
   * on the bus, so returning TRUE (FALSE means we want to stop watching
   * for messages on the bus and our callback should not be called again)
   */
  return TRUE;
}

gint main (gint argc, gchar * argv[])
{
  GstElement *pipeline;
  GstBus *bus;
  guint bus_watch_id;

  /* init */
  gst_init (&argc, &argv);

  /* create pipeline, add handler */
  pipeline = gst_pipeline_new ("my_pipeline");

  /* adds a watch for new message on our pipeline's message bus to
   * the default GLib main context, which is the main context that our
   * GLib main loop is attached to below
   */
  bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
  bus_watch_id = gst_bus_add_watch (bus, my_bus_callback, NULL);
  gst_object_unref (bus);

  /* [...] */

  /* create a mainloop that runs/iterates the default GLib main context
   * (context NULL), in other words: makes the context check if anything
   * it watches for has happened. When a message has been posted on the
   * bus, the default main context will automatically call our
   * my_bus_callback() function to notify us of that message.
   * The main loop will be run until someone calls g_main_loop_quit()
   */
  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

  /* clean up */
  gst_element_set_state (pipeline, GST_STATE_NULL);
  gst_object_unref (pipeline);
  g_source_remove (bus_watch_id);
  g_main_loop_unref (loop);

  return 0;
}

請注意,如果你使用gst_bus_add_signal_watch(),則可以連接總線上的“消息”信號,而不必switch()處理所有可能的消息類型。只需以 message::的形式連接到你需要處理的信號。

上面的代碼段也可以寫成:

GstBus *bus;

[..]

bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
gst_bus_add_signal_watch (bus);
g_signal_connect (bus, "message::error", G_CALLBACK (cb_message_error), NULL);
g_signal_connect (bus, "message::eos", G_CALLBACK (cb_message_eos), NULL);

[..]

Bus Message類型

GStreamer具有一些可以通過總線傳遞的預定義消息類型。 所有消息都有消息源,類型和時間戳。消息源可以被用於查看哪些元件發出的消息。

以下是所有消息的列表,並簡要說明了它們的作用以及如何解析消息內容。

  • 錯誤,警告和信息通知:如果應向用戶顯示有關管道狀態的消息,則元素使用這些通知。錯誤消息是致命的,並終止數據傳遞。警告不是致命的,但仍然暗示着問題。信息消息用於非問題通知。可以使用gst_message_parse_error()gst_message_parse_warning ()gst_message_parse_info ()來解析這些消息。使用後應釋放錯誤和調試這些消息對象。
  • 流結束通知:流結束時發出。管道的狀態不會改變,但是進一步的媒體處理將停止。
  • 標籤:在流中找到元數據時發出。應用程序應在內部緩存元數據。gst_message_parse_tag()被用於解析tag列表,gst_tag_list_unref ()當不再需要時應進行釋放。
  • 狀態更改:狀態更改成功後發出。 gst_message_parse_state_changed ()可用於解析舊狀態和新狀態的轉變。
  • 緩衝:在網絡流緩存期間發出。通過gst_message_get_structure()函數從返回的結構中提取“ buffer-percent”屬性,可以從消息中手動提取媒體進度(以百分比爲單位)。
  • 特定於應用程序的消息:可以通過獲取消息結構並讀取其字段來提取有關這些消息的任何信息。應用程序消息主要用於應用程序內部使用,以防應用程序需要將信息從某個線程封送到主線程中。

3.5 最佳實戰

#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
  GstElement *pipeline;
  GstElement *source;
  GstElement *convert;
  GstElement *sink;
} CustomData;

/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);

int main(int argc, char *argv[]) {
  CustomData data;
  GstBus *bus;
  GstMessage *msg;
  GstStateChangeReturn ret;
  gboolean terminate = FALSE;

  /* Initialize GStreamer */
  gst_init (&argc, &argv);

  /* Create the elements */
  data.source = gst_element_factory_make ("uridecodebin", "source");
  data.convert = gst_element_factory_make ("audioconvert", "convert");
  data.sink = gst_element_factory_make ("autoaudiosink", "sink");

  /* Create the empty pipeline */
  data.pipeline = gst_pipeline_new ("test-pipeline");

  if (!data.pipeline || !data.source || !data.convert || !data.sink) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

  /* Build the pipeline. Note that we are NOT linking the source at this
   * point. We will do it later. */
  gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert , data.sink, NULL);
  if (!gst_element_link (data.convert, data.sink)) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  /* Set the URI to play */
  g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

  /* Connect to the pad-added signal */
  g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

  /* Start playing */
  ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    g_printerr ("Unable to set the pipeline to the playing state.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  /* Listen to the bus */
  bus = gst_element_get_bus (data.pipeline);
  do {
    msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
        GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

    /* Parse message */
    if (msg != NULL) {
      GError *err;
      gchar *debug_info;

      switch (GST_MESSAGE_TYPE (msg)) {
        case GST_MESSAGE_ERROR:
          gst_message_parse_error (msg, &err, &debug_info);
          g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
          g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
          g_clear_error (&err);
          g_free (debug_info);
          terminate = TRUE;
          break;
        case GST_MESSAGE_EOS:
          g_print ("End-Of-Stream reached.\n");
          terminate = TRUE;
          break;
        case GST_MESSAGE_STATE_CHANGED:
          /* We are only interested in state-changed messages from the pipeline */
          if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) {
            GstState old_state, new_state, pending_state;
            gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
            g_print ("Pipeline state changed from %s to %s:\n",
                gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
          }
          break;
        default:
          /* We should not reach here */
          g_printerr ("Unexpected message received.\n");
          break;
      }
      gst_message_unref (msg);
    }
  } while (!terminate);

  /* Free resources */
  gst_object_unref (bus);
  gst_element_set_state (data.pipeline, GST_STATE_NULL);
  gst_object_unref (data.pipeline);
  return 0;
}

/* This function will be called by the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
  GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");
  GstPadLinkReturn ret;
  GstCaps *new_pad_caps = NULL;
  GstStructure *new_pad_struct = NULL;
  const gchar *new_pad_type = NULL;

  g_print ("Received new pad '%s' from '%s':\n", GST_PAD_NAME (new_pad), GST_ELEMENT_NAME (src));

  /* If our converter is already linked, we have nothing to do here */
  if (gst_pad_is_linked (sink_pad)) {
    g_print ("We are already linked. Ignoring.\n");
    goto exit;
  }

  /* Check the new pad's type */
  new_pad_caps = gst_pad_get_current_caps (new_pad);
  new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
  new_pad_type = gst_structure_get_name (new_pad_struct);
  if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) {
    g_print ("It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
    goto exit;
  }

  /* Attempt the link */
  ret = gst_pad_link (new_pad, sink_pad);
  if (GST_PAD_LINK_FAILED (ret)) {
    g_print ("Type is '%s' but link failed.\n", new_pad_type);
  } else {
    g_print ("Link succeeded (type '%s').\n", new_pad_type);
  }

exit:
  /* Unreference the new pad's caps, if we got them */
  if (new_pad_caps != NULL)
    gst_caps_unref (new_pad_caps);

  /* Unreference the sink pad */
  gst_object_unref (sink_pad);
}

編譯方法:

gcc basic-tutorial-3.c -o basic-tutorial-3 `pkg-config --cflags --libs gstreamer-1.0`

代碼註釋:

/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
  GstElement *pipeline;
  GstElement *source;
  GstElement *convert;
  GstElement *sink;
} CustomData;

由於本教程(以及大多數實際應用程序)都涉及回調,因此我們將所有數據放在一個結構體中,以便於傳遞處理。

/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);

這是信號處理函數,將在信號觸發後調用,此處爲函數預定義。

/* Create the elements */
data.source = gst_element_factory_make ("uridecodebin", "source");
data.convert = gst_element_factory_make ("audioconvert", "convert");
data.sink = gst_element_factory_make ("autoaudiosink", "sink");

創建元素。uridecodebin將在內部實例化所有必要的元素(source,demuxer和decode),以將URI轉換爲原始音頻或視頻流。由於它包含demuxer,因此其src pad最初不可用,我們需要動態創建src pad。

audioconvert 用於轉換不同的音頻格式。

autoaudiosink用於將音頻流呈現到聲卡。

if (!gst_element_link (data.convert, data.sink)) {
  g_printerr ("Elements could not be linked.\n");
  gst_object_unref (data.pipeline);
  return -1;
}

在這裏,我們將audioconvert元素鏈接到autoaudiosink元素,但是我們不將它們與uridecodebin鏈接,因爲此時uridecodebin 不包含src pad。

/* Set the URI to play */
g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

我們將文件的URI設置爲通過屬性播放。

信號處理

/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

GSignals是GStreamer的一個關鍵點,它們使你可以在發生某些事情的時候(通過回調)得到通知並進行處理。

在這一行中,通過g_signal_connect監聽"pad-added"信號,並提供要使用的回調函數pad_added_handler和數據指針。GStreamer對此數據指針不執行任何操作,它只是將其轉發給回調,因此我們可以與其共享信息。在這種情況下,我們傳遞一個指向CustomData的專門構建的指針。

GstElement生成的信號可以在其文檔中找到,也可以使用gst-inspect-1.0找到。

/* Start playing */
  ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    g_printerr ("Unable to set the pipeline to the playing state.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }
/* Listen to the bus */
  bus = gst_element_get_bus (data.pipeline);

將管道設置爲PLAYING狀態,並開始偵聽總線中是否有消息(例如ERROREOS)。

回調

uridecodebin元素最終有足夠的流信息時,它將創建src pad,並觸發“pad-added”信號。此時將執行回調函數。

static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {

src是觸發信號的GstElement,在此示例中,它只能是uridecodebin。信號處理程序的第一個參數始終是觸發它的對象。

new_pad是剛剛添加到src元素中的GstPad,通常這是我們要鏈接的pad。

data是我們在連接信號時提供的指針。在此示例中,我們使用它傳遞CustomData指針。

GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink");

CustomData中,我們提取出convert元素,然後使用gst_element_get_static_pad ()檢索其sink pad。我們需要把uridecodebin新生成的new_pad(src pad)和audioconvert的sink_pad鏈接到一起。

/* If our converter is already linked, we have nothing to do here */
if (gst_pad_is_linked (sink_pad)) {
  g_print ("We are already linked. Ignoring.\n");
  goto exit;
}

uridecodebin會嘗試創建多個pad,並且對於每個pad,都會調用此回調。一旦我們已經鏈接成功,上面的代碼將阻止繼續鏈接。

/* Check the new pad's type */
new_pad_caps = gst_pad_get_current_caps (new_pad, NULL);
new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
new_pad_type = gst_structure_get_name (new_pad_struct);
if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) {
  g_print ("It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
  goto exit;
}

現在,我們將檢查新創建的src pad將要輸出的數據類型,我們僅對產生音頻的pad感興趣,因此使用g_str_has_prefix (new_pad_type, “audio/x-raw”)進行過濾。

gst_pad_get_current_caps()檢索pad 的當前capabilities(即其當前輸出的數據類型)。gst_caps_get_structure()檢索new_pad_caps的GstStructure結構體數據 。gst_structure_get_name()得到new pad的類型。

如果名稱不是audio/x-raw,則該名稱不是經過解碼的音頻pad,因此我們忽略它。否則,嘗試鏈接pad:

/* Attempt the link */
ret = gst_pad_link (new_pad, sink_pad);
if (GST_PAD_LINK_FAILED (ret)) {
  g_print ("Type is '%s' but link failed.\n", new_pad_type);
} else {
  g_print ("Link succeeded (type '%s').\n", new_pad_type);
}

gst_pad_link()嘗試鏈接兩個pad。與gst_element_link()`一樣,必須從src鏈接到sink,並且兩個pad都必須位於同一bin(或管道)中。

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