在音视频领域接触最多实现的方案通常是通过ffmpeg(PC和sever端居多)或者硬件厂家的的SDK实现特定硬件的编解码功能(机顶盒,电视等嵌入式设备)。国内不太常用的解决方案gstreamer

gstreamer跟ffmpeg一样,也是一个媒体框架,可以实现采集,编码,解码,渲染,滤镜等一条龙的媒体解决方案。 gstreamer基于glib实现,用C语言来实现面向对象思维,完全不是标准C++那一套逻辑,由于要跨平台,原生的系统API都是适配封装了一套,甚至自己实现队列,MAP,容器,协程,线程,异步操作,不熟悉glib 的API话,代码理解比较困难,用惯了C++,STL,boost,感觉得这是gstream最让人反感的一点,不合主流,搞的又要学一套API。 Gstreamer采用插件管理各个模块,软件框架比较复杂,采用了异步,协程编程模型,进一步增加了理解难度。

环境:

apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

什么是Gstreamer?

Gstreamer是一个支持Windows,Linux,Android, iOS的跨平台的多媒体框架,应用程序可以通过 管道(Pipeline) 的方式,将多媒体处理的各个步骤串联起来,达到预期的效果。每个步骤通过元素(Element)基于GObject对象系统通过插件(plugins)的方式实现,方便了各项功能的扩展。


Media Applications
最上面一层为应用,比如gstreamer自带的一些工具(gst-launch,gst-inspect等),以及基于gstreamer封装的(gst-player,gst-rtsp-server,gst-editing-services等)根据不同场景实现的应用。
Core Framework
中间一层为Core Framework,主要提供:

  • 上层应用所需接口
  • Plugin的框架
  • Pipline的框架
  • 数据在各个Element间的传输及处理机制
  • 多个媒体流(Streaming)间的同步(比如音视频同步)
  • 其他各种所需的工具库

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等。
Gstreamer框架根据各个模块的成熟度以及所使用的开源协议,将core及plugins置于不同的源码包中:
gstreamer: 包含core framework及core elements。
gst-plugins-base: gstreamer应用所需的必要插件。
gst-plugins-good: 高质量的采用LGPL授权的插件。
gst-plugins-ugly: 高质量,但使用了GPL等其他授权方式的库的插件,比如使用GPL的x264,x265。
gst-plugins-bad: 质量有待提高的插件,成熟后可以移到good插件列表中。
gst-libav: 对libav封装,使其能在gstreamer框架中使用。

Gstreamer基础概念

Element

Element(GstElement)是Gstreamer中最重要的对象类型之一。一个element实现一个功能(读取文件,解码,输出等),程序需要创建多个element,并按顺序将其串连起来,构成一个完整的pipeline。
GStreamer提供的解码器(decoder),编码器(encoder), 分离器(demuxer), 音视频输出设备都是派生自GstElement。

Pad

Pad是一个element的输入/输出接口,分为src pad(生产数据)sink pad(消费数据)两种。
两个element必须通过pad才能连接起来,pad拥有当前element能处理数据类型的能力(capabilities),会在连接时通过比较src pad和sink pad中所支持的能力,来选择最恰当的数据类型用于传输,如果element不支持,程序会直接退出。在element通过pad连接成功后,数据会
从上一个element的src pad传到下一个element的sink pad
然后进行处理。
当element支持多种数据处理能力时,我们可以通过Cap来指定数据类型.
例如,下面的命令通过Cap指定了视频的宽高,videotestsrc会根据指定的宽高产生相应数据:

gst-launch-1.0 videotestsrc ! "video/x-raw,width=1280,height=720" ! autovideosink

Bin和Pipeline

Bin是一个容器,用于管理多个element,改变bin的状态时,bin会自动去修改所包含的element的状态,也会转发所收到的消息。如果没有bin,我们需要依次操作我们所使用的element。通过bin降低了应用的复杂度。
Pipeline继承自bin,为程序提供一个bus用于传输消息,并且对所有子element进行同步。当将pipeline的状态设置为PLAYING时,pipeline会在一个/多个新的线程中通过element处理数据。

下面我们通过一个文件播放的例子来熟悉上述提及的概念:sintel_trailer-480p.ogv

gst-launch-1.0 filesrc location=sintel_trailer-480p.ogv ! oggdemux name=demux ! queue ! vorbisdec ! autoaudiosink demux. ! queue ! theoradec ! videoconvert ! autovideosink

通过上面的命令播放文件时,会创建如下pipeline:

可以看到这个pipeline由8个element构成,每个element都实现各自的功能:filesrc读取文件,oggdemux解析文件,分别提取audio,video数据,queue缓存数据,vorbisdec解码audio,autoaudiosink自动选择音频设备并输出,theoradec解码video,videoconvert转换video数据格式,autovideosink自动选择显示设备并输出。
不同的element拥有不同数量及类型的pad,只有src pad的element被称为source element,只有sink pad的被称为sink element。element可以同时拥有多个相同的pad,例如oggdemux在解析文件后,会将audio,video通过不同的pad输出。

Gstreamer数据消息交互

在pipeline运行的过程中,各个element以及应用之间不可避免的需要进行数据消息的传输,gstreamer提供了bus系统以及多种数据类型(Buffers、Events、Messages,Queries)来达到此目的:

Bus
Bus是gstreamer内部用于将消息从内部不同的streaming线程,传递到bus线程,再由bus所在线程将消息发送到应用程序。应用程序只需要向bus注册消息处理函数,即可接收到pipline中各element所发出的消息,使用bus后,应用程序就不用关心消息是从哪一个线程发出的,避免了处理多个线程同时发出消息的复杂性。

Buffers
用于从sources到sinks的媒体数据传输。

Events
用于element之间或者应用到element之间的信息传递,比如播放时的seek操作是通过event实现的。

Messages
是由element发出的消息,通过bus,以异步的方式被应用程序处理。通常用于传递errors, tags, state changes, buffering state, redirects等消息。消息处理是线程安全的。由于大部分消息是通过异步方式处理,所以会在应用程序里存在一点延迟,如果要及时的相应消息,需要在streaming线程捕获处理。

Queries
用于应用程序向gstreamer查询总时间,当前时间,文件大小等信息。

gstreamer tools
Gstreamer自带了gst-inspect-1.0和gst-launch-1.0等其他命令行工具,我们可以使用这些工具完成常见的处理任务。
gst-inspect-1.0
查看gstreamer的plugin、element的信息。直接将plugin/element的类型作为参数,会列出其详细信息。如果不跟任何参数,会列出当前系统gstreamer所能查找到的所有插件。
gst-launch-1.0
用于创建及执行一个Pipline,因此通常使用gst-launch先验证相关功能,然后再编写相应应用。
通过上面ogg视频播放的例子,我们已经看到,一个pipeline的多个element之间通过 “!" 分隔,同时可以设置element及Cap的属性。例如:
播放音视频

gst-launch-1.0 playbin file:///home/root/test.mp4

转码

gst-launch-1.0 filesrc location=/videos/sintel_trailer-480p.ogv ! decodebin name=decode ! \
               videoscale ! "video/x-raw,width=320,height=240" ! x264enc ! queue ! \
               mp4mux name=mux ! filesink location=320x240.mp4 decode. ! audioconvert ! \
               avenc_aac ! queue ! mux.

Streaming

#Server
gst-launch-1.0 -v videotestsrc ! "video/x-raw,framerate=30/1" ! x264enc key-int-max=30 ! rtph264pay ! udpsink host=127.0.0.1 port=1234

#Client
gst-launch-1.0 udpsrc port=1234 ! "application/x-rtp, payload=96" ! rtph264depay ! decodebin ! autovideosink sync=false

Hello,world

#include <gst/gst.h>

int main (int argc, char *argv[])
{
  GstElement *pipeline;
  GstBus *bus;
  GstMessage *msg;

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

  /* Build the pipeline */
  pipeline = gst_parse_launch ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

  /* Start playing */
  gst_element_set_state (pipeline, GST_STATE_PLAYING);

  /* Wait until error or EOS */
  bus = gst_element_get_bus (pipeline);
  msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
      GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

  /* Free resources */
  if (msg != NULL)
    gst_message_unref (msg);
  gst_object_unref (bus);
  gst_element_set_state (pipeline, GST_STATE_NULL);
  gst_object_unref (pipeline);
  return 0;
}
gst_init (&argc, &argv);

初始化函数必须在其他gstreamer接口之前被调用,gst_init会负责以下资源的初始化:

  • 初始化GStreamer库/注册内部element/加载插件列表,扫描列表中及相应路径下的插件/解析并执行命令行参数
    在不需要gst_init处理命令行参数时,我们可以讲NULL作为其参数,例如:gst_init(NULL, NULL);
pipeline = gst_parse_launch ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

创建Pipeline,通过gst_parse_launch 创建一个playbin的pipeline,并设置播放文件的uri。
在pipeline中,首先通过“source” element获取媒体数据,然后通过一个或多个element对编码数据进行解码,最后通过“sink” element输出声音和画面。
在创建较复杂的pipeline时,我们需要通过gst_element_factory_make来创建element,然后将其加入到GStreamer Bin中,并连接起来。当pipeline比较简单并且我们不需要对pipeline中的element进行过多的控制时,我们可以采用gst_parse_launch来简化pipeline的创建。
这个函数能够巧妙的将pipeline的文本描述转化为pipeline对象,我们也经常需要通过文本方式构建pipeline来查看GStreamer是否支持相应的功能,因此GStreamer提供了gst-launch-1.0命令行工具,极大的方便了pipeline的测试。

pipeline中需要添加特定的element以实现相应的功能,在本例中,我们通过gst_parse_launch创建了只包含一个element的Pipeline。
我们刚提到pipeline需要有“source”、“sink” element,为什么这里只需要一个playbin就够了呢?
是因为playbin element内部会根据文件的类型自动去查找所需要的“source”,“decoder”,”sink”并将它们连接起来,同时提供了部分接口用于控制pipeline中相应的element。在playbin后,我们跟了一个uri参数,指定了我们想要播放的媒体文件地址,playbin会根据uri所使用的协议(“https://”,“ftp://”,“file://”等)自动选择合适的source element(此例中通过https方式)获取数据。

gst_element_set_state (pipeline, GST_STATE_PLAYING);

每个GStreamer element都有相应都状态(state),我们目前可以简单的把状态与播放器的播放/暂停按钮联系起来,只有当状态处于PLAYING时,pipeline才会播放/处理数据。
这里gst_element_set_state通过pipeline,将playbin的状态设置为PLAYING,使playbin开始播放视频文件。

/* Wait until error or EOS */
bus = gst_element_get_bus (pipeline);
msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

GStreamer框架会通过bus,将所发生的事件通知到应用程序,因此,这里首先取得pipeline的bus对象,通过gst_bus_timed_pop_filtered 以同步的方式等待bus上的ERROR或EOS(End of Stream)消息,该函数收到消息后才会返回。GStreamer会处理视频播放的所有工作 (数据获取,解码,音视频同步,输出) 。当到达文件末端(EOS)或出错 (直接关闭播放窗口,断开网络)时,播放会自动停止。我们也可以在终端通过ctrl+c中断程序的执行。

/* Free resources */
if (msg != NULL)
  gst_message_unref (msg);

gst_object_unref (bus);
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);

这里我们将不再使用的msg,bus对象进行销毁,并将pipeline状态设置为NULL(在NULL状态时GStreamer会释放为pipeline分配的所有资源),最后销毁pipeline对象。由于GStreamer是继承自GObject,所以需要通过gst_object_unref 来减少引用计数,当对象的引用计数为0时,函数内部会自动释放为其分配的内存。不同接口会对返回的对象进行不同的处理,我们需要详细的阅读API文档,来决定我们是否需要对返回的对象进行释放。

进阶 - Element & pipeline

为了能够更好的控制pipline中的element,我们需要单独创建element,然后再构造pipeline
Element是一个功能基本单元,可以分为:

  1. source element
    只能生成数据,不能接收数据的element称为source element。例如用于文件读取的filesrc等。
  2. sink element
    只能接收数据,不能产生数据的element称为sink element。例如播放声音的alsasink等。
  3. filter-like element
    既能接收数据,又能生成数据的element称为filter-like element。例如分离器,解码器,音量控制器等。对于filter-like element,既包含位于element左边的sink pad,又包含位于element右边的src pad。

    对于这些的element,可能包含多个src pad,也可能包含多个sink pad,例如mp4的demuxer(qtdemux)会将mp4文件中的音频和视频的分离到audio src pad和video src pad。而mp4的muxer(mp4mux)则相反,会将audio sink pad和video sink pad的数据合并到一个src pad,再经其他element将数据写入文件或发送到网络。demuxer如下图:

    将element串联起来后就能实现相应的功能,为什么我们还需要bin和pipline呢?我们首先来看看在GStreamer框架中element,bin,pipeline对象之间的继承关系:
GObject
    ╰──GInitiallyUnowned
        ╰──GstObject
            ╰──GstElement
                ╰──GstBin
                    ╰──GstPipeline

bin和pipeline都是一个element,那么bin和pipeline都在element的基础上实现了什么功能,解决了什么问题呢?
我们在创建了element多个element后,我们需要对element进行状态/资源管理,如果每次状态改变时,都需要依次去操作每个element,这样每次编写一个应用都会有大量的重复工作,这时就有了bin。
Bin继承自element后,实现了容器的功能,可以将多个element添加到bin,当操作bin时,bin会将相应的操作转发到内部所有的element中,我们可以将bin认为认为是一个新的逻辑element,由bin来管理其内部element的状态及资源,转发其产生的消息。常见的bin有decodebin,autovideoconvert等。

Bin实现了容器的功能,那pipeline又有什么功能呢?
在多媒体应用中,音视频同步是一个基本的功能,需要支持这样的功能,所有的element必须要有一个相同的时钟,这样才能保证各个音频和视频在同一时间输出。pipeline就会为其内部所有的element选择一个相同的时钟,同时还为应用提供了bus系统,用于消息的接收。
这个pipeline上所有的element都可以使用这个bus向应用程序发送消息。 Bus主要是为了解决多线程之间消息处理的问题。 由于GStreamer内部可能会创建多个线程,如果没有bus,应用程序可能同时收到从多个线程的消息,如果应用程序在发送线程中通过回调去处理消息,应用程序有可能阻塞播放线程,造成播放卡顿,死锁等其他问题。为了解决这类问题,GStreamer通常是将多个线程的消息发送到bus系统,由应用程序从bus中取出消息,然后进行处理。Bus在这里扮演了消息队列的角色,通过bus解耦了GStreamer框架和应用程序对消息的处理,降低了应用程序的复杂度。

demo2:element创建及使用,基于gst_element_factory_make

#include <gst/gst.h>

int main (int argc, char *argv[])
{
  GstElement *pipeline, *source, *filter, *sink;
  GstBus *bus;
  GstMessage *msg;
  GstStateChangeReturn ret;

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

  /* Create the elements */
  source = gst_element_factory_make ("videotestsrc", "source"); // 产生视频数据,通常用于调试。
  filter = gst_element_factory_make ("timeoverlay", "filter"); // 在视频数据中叠加一个时间字符串
  sink = gst_element_factory_make ("autovideosink", "sink"); // 自动选择视频输出设备,创建视频显示窗口,并显示其收到的数据
  /*
	第一个参数是element的类型,可以通过这个字符串,找到对应的类型,从而创建element对象。
	第二个参数指定了创建element的名字,当我们没有保存创建element的对象指针时,我们可以通过gst_bin_get_by_name从pipeline中取得该element的对象指针。
	如果第二个参数为NULL,则GStreamer内部会为该element自动生成一个唯一的名字。
	*/
  /* Create the empty pipeline */
  pipeline = gst_pipeline_new ("test-pipeline"); // 创建Pipeline

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

  /* Build the pipeline */
  gst_bin_add_many (GST_BIN (pipeline), source, filter, sink, NULL);//把我们创建的element添加到pipeline中
  if (gst_element_link_many (source, filter, sink, NULL) != TRUE) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (pipeline);
    return -1;
  }

  /* Modify the source's properties */
  g_object_set (source, "pattern", 0, NULL); // pattern属性可以控制测试图像的类型
  /*
  由于GstElement继承于GObject,同时GObject对象系统提供了 g_object_get()用于读取属性,g_object_set()用于修改属性,g_object_set()支持以NULL结束的属性-值的键值对,所以可以一次修改element的多个属性。
	*/
  /* Start playing */
  ret = gst_element_set_state (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 (pipeline);
    return -1;
  }

  /* Wait until error or EOS */
  bus = gst_element_get_bus (pipeline);
  msg =
      gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
      GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
// 由于videotestsrc会持续输出视频数据,所以我们在这个例子中不会受到EOS消息,只有当我们关闭视频窗口时会收到error消息,发送消息的element名和具体的消息内容会通过终端输出。
  /* 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);
        break;
      case GST_MESSAGE_EOS:
        g_print ("End-Of-Stream reached.\n");
        break;
      default:
        /* We should not reach here because we only asked for ERRORs and EOS */
        g_printerr ("Unexpected message received.\n");
        break;
    }
    gst_message_unref (msg);
  }

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

gst_bin_add_many (GST_BIN (pipeline), source, filter, sink, NULL)
pipeline是继承自bin,所以所有bin的方法都可以应用于pipeline,需要注意的是,我们需要通过相应的宏(这里是GST_BIN)来将子类转换为父类,宏内部会对其做类型检查。在这里我们使用gst_bin_add_many将多个element加入到pipeline中,这个函数接受任意多个参数,最后以NULL表示参数列表的结束。如果一次只需要加入一个,可以使用gst_bin_add函数。在将element加入bin后,我们需要将其连接起来才能完成相应的功能,由于有多个element,所以我们这里使用gst_element_link_many,element会根据参数的顺序依次将element连接起来。

Pad

pad是element之间的数据的接口,一个src pad只能与一个sink pad相连。每个element可以通过pad过滤数据,接收自己支持的数据类型。Pad通过Pad Capabilities(简称为Pad Caps)来描述支持的数据类型。例如:

  • 表示分辨率为300x200,帧率为30fps的RGB视频的Caps:  “video/x-raw,format=RGB,width=300,height=200,framerate=30/1”
  • 表示采样位宽为16位,采样率44.1kHz,双通道PCM音频的Caps: “audio/x-raw,format=S16LE,rate=44100,channels=2”
  • 直接描述编码数据格式Voribis,VP8: “audio/x-vorbis” “video/x-vp8”

Element间的连接,实质上就是pad间的连接,caps适配
一个Pad可以支持多种类型的Caps(比如一个video sink可以同时支持RGB或YUV格式的数据),同时可以指定Caps支持的数据范围(比如一个audio sink可以支持1~48k的采样率)。但是,在一个Pipeline中,Pad之间所传输的数据类型必须是唯一的。 GStreamer在进行element连接时,会通过 协商(negotiation) 的方式选择一个双方都支持的类型。为了能使两个Element能够正确的连接,双方的Pad Caps之间必须有交集,从而在协商阶段选择相同的数据类型,这就是Pad Caps的主要作用。 在实际使用中,我们可以通过gst-inspect工具查看Element所支持的Pad Caps,从而才能知道在连接出错时如何处理。

Pad Templates

使用gst_element_factory_make()接口创建Element,这个接口内部也会先创建一个Element 工厂,再通过工厂方法创建一个Element。由于大部分Element都需要创建类似的Pad,于是GStreame定义了Pad Template,Pad Template被包含中Element工厂中,在创建Element时,用于快速创建Pad。Pad Template包含了一个Pad所能支持的所有Caps。通过Pad Template,我们可以快速的判断两个pad是否能够连接(比如两个elements都只提供了sink template,这样的element之间是无法连接的,这样就没必要进一步判断Pad Caps)。由于Pad Template属于Element工厂,所以我们可以直接使用gst-inspect查看其属性,但Element实际的Pad会根据Element所处的不同状态来进行实例化,具体的Pad Caps会在协商后才会被确定。

demo:Pad Templates Capabilities例子

gst-inspect-1.0 alsasink

out:

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      audio/x-raw
                 format: S16LE
                 layout: interleaved
                   rate: [ 1, 48000 ]
               channels: [ 1, 2 ]
      audio/x-ac3
                 framed: true

alsasink只提供了一个sink template,可以创建sink pad。支持两种类型的音频数据:(1)16位的PCM(audio/x-raw),采样率1~48k,1-2通道 (2)AC3(audio/x-ac3)的帧数据。

gst-inspect-1.0 videotestsrc
Pad Templates:
  SRC template: 'src'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { I420, YV12, YUY2, UYVY, AYUV, RGBx, BGRx, xRGB, xBGR, RGBA, BGRA, ARGB, ABGR, RGB, BGR, Y41B, Y42B, YVYU, Y444, v210, v216, NV12, NV21, NV16, NV24, GRAY8, GRAY16_BE, GRAY16_LE, v308, RGB16, BGR16, RGB15, BGR15, UYVP, A420, RGB8P, YUV9, YVU9, IYU1, ARGB64, AYUV64, r210, I420_10LE, I420_10BE, I422_10LE, I422_10BE, Y444_10LE, Y444_10BE, GBR, GBR_10LE, GBR_10BE }
                  width: [ 1, 2147483647 ]
                 height: [ 1, 2147483647 ]
              framerate: [ 0/1, 2147483647/1 ]
      video/x-bayer
                 format: { bggr, rggb, grbg, gbrg }
                  width: [ 1, 2147483647 ]
                 height: [ 1, 2147483647 ]
              framerate: [ 0/1, 2147483647/1 ]

videotestsrc只提供了一个src template用于创建src pad,pad支持多种格式,可以通过参数指定输出的数据类型或Caps Filter指定。

Pad Availability

上面的例子中显示的Pad Template都是一直存在的(Availability: Always),创建的Pad也是一直有效的。但有些Element会根据输入数据以及后续的Element动态增加或删除Pad,因此GStreamer提供了3种Pad有效性的状态:Always,Sometimes,On request

Always Pad:在element被初始化后就存在的pad,被称为always pad或static pad。
Sometimes Pad:根据输入数据的不同而产生的pad,被称为sometimes pad,常见于各种文件格式解析器
例如用于解析mp4文件的qtdemux:“gst-inspect-1.0 qtdemux”

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      video/quicktime
      video/mj2
      audio/x-m4a
      application/x-3gp

  SRC template: 'video_%u'
    Availability: Sometimes
    Capabilities:
      ANY

  SRC template: 'audio_%u'
    Availability: Sometimes
    Capabilities:
      ANY

  SRC template: 'subtitle_%u'
    Availability: Sometimes
    Capabilities:
      ANY

只有我们从mp4文件中读取数据时,我们才能知道这个文件中包含多少音频,视频,字幕,所以这些src pad都是sometimes pad。
Request Pad:按需创建的pad被称为request pad,常见于合并或生成多路数据。例如,用于1到N转换的tee:“gst-inspect-1.0 tee”

Pad Templates:
  ...
  SRC template: 'src_%u'
    Availability: On request
      Has request_new_pad() function: gst_tee_request_new_pad
    Capabilities:
      ANY

当我们需要将同一路视频流同时进行显示和存储,这时候我们就需要用到tee,在创建tee element的时候,我们不知道pipeline需要多少个src pad,需要后续element来请求一个src pad。


GStreamer提供了gst-inspect工具来查看element所提供的Pad Templates,但无法查看element在不同状态时其Pad所支持的数据类型,通过下面的代码,我们可以看到Pad Caps在不同状态下的变化。

开头的三个函数print_field, print_caps and print_pad_templates_information实现类似功能,打印GStreamer的数据结构GstCaps

#include <gst/gst.h>

/* Functions below print the Capabilities in a human-friendly format */
static gboolean print_field (GQuark field, const GValue * value, gpointer pfx) {
  gchar *str = gst_value_serialize (value);

  g_print ("%s  %15s: %s\n", (gchar *) pfx, g_quark_to_string (field), str);
  g_free (str);
  return TRUE;
}

static void print_caps (const GstCaps * caps, const gchar * pfx) {
  guint i;

  g_return_if_fail (caps != NULL);

  if (gst_caps_is_any (caps)) {
    g_print ("%sANY\n", pfx);
    return;
  }
  if (gst_caps_is_empty (caps)) {
    g_print ("%sEMPTY\n", pfx);
    return;
  }

  for (i = 0; i < gst_caps_get_size (caps); i++) {
    GstStructure *structure = gst_caps_get_structure (caps, i);

    g_print ("%s%s\n", pfx, gst_structure_get_name (structure));
    gst_structure_foreach (structure, print_field, (gpointer) pfx);
  }
}

/* Prints information about a Pad Template, including its Capabilities */
static void print_pad_templates_information (GstElementFactory * factory) {
  const GList *pads;
  GstStaticPadTemplate *padtemplate;

  g_print ("Pad Templates for %s:\n", gst_element_factory_get_longname (factory));
  if (!gst_element_factory_get_num_pad_templates (factory)) {
    g_print ("  none\n");
    return;
  }

  pads = gst_element_factory_get_static_pad_templates (factory);
  while (pads) {
    padtemplate = pads->data;
    pads = g_list_next (pads);

    if (padtemplate->direction == GST_PAD_SRC)
      g_print ("  SRC template: '%s'\n", padtemplate->name_template);
    else if (padtemplate->direction == GST_PAD_SINK)
      g_print ("  SINK template: '%s'\n", padtemplate->name_template);
    else
      g_print ("  UNKNOWN!!! template: '%s'\n", padtemplate->name_template);

    if (padtemplate->presence == GST_PAD_ALWAYS)
      g_print ("    Availability: Always\n");
    else if (padtemplate->presence == GST_PAD_SOMETIMES)
      g_print ("    Availability: Sometimes\n");
    else if (padtemplate->presence == GST_PAD_REQUEST)
      g_print ("    Availability: On request\n");
    else
      g_print ("    Availability: UNKNOWN!!!\n");

    if (padtemplate->static_caps.string) {
      GstCaps *caps;
      g_print ("    Capabilities:\n");
      caps = gst_static_caps_get (&padtemplate->static_caps);
      print_caps (caps, "      ");
      gst_caps_unref (caps);

    }

    g_print ("\n");
  }
}

/* Shows the CURRENT capabilities of the requested pad in the given element */
static void print_pad_capabilities (GstElement *element, gchar *pad_name) {
  GstPad *pad = NULL;
  GstCaps *caps = NULL;

  /* Retrieve pad */
  pad = gst_element_get_static_pad (element, pad_name);
  if (!pad) {
    g_printerr ("Could not retrieve pad '%s'\n", pad_name);
    return;
  }

  /* Retrieve negotiated caps (or acceptable caps if negotiation is not finished yet) */
  caps = gst_pad_get_current_caps (pad);
  if (!caps)
    caps = gst_pad_query_caps (pad, NULL);

  /* Print and free */
  g_print ("Caps for the %s pad:\n", pad_name);
  print_caps (caps, "      ");
  gst_caps_unref (caps);
  gst_object_unref (pad);
}

int main(int argc, char *argv[]) {
  GstElement *pipeline, *source, *sink;
  GstElementFactory *source_factory, *sink_factory;
  GstBus *bus;
  GstMessage *msg;
  GstStateChangeReturn ret;
  gboolean terminate = FALSE;

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

  /* Create the element factories */
  source_factory = gst_element_factory_find ("audiotestsrc");
  sink_factory = gst_element_factory_find ("autoaudiosink");
  if (!source_factory || !sink_factory) {
    g_printerr ("Not all element factories could be created.\n");
    return -1;
  }

  /* Print information about the pad templates of these factories */
  print_pad_templates_information (source_factory);
  print_pad_templates_information (sink_factory);

  /* Ask the factories to instantiate actual elements */
  source = gst_element_factory_create (source_factory, "source");
  sink = gst_element_factory_create (sink_factory, "sink");

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

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

  /* Build the pipeline */
  gst_bin_add_many (GST_BIN (pipeline), source, sink, NULL);
  if (gst_element_link (source, sink) != TRUE) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (pipeline);
    return -1;
  }

  /* Print initial negotiated caps (in NULL state) */
  g_print ("In NULL state:\n");
  print_pad_capabilities (sink, "sink");

  /* Start playing */
  ret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    g_printerr ("Unable to set the pipeline to the playing state (check the bus for error messages).\n");
  }

  /* Wait until error, EOS or State Change */
  bus = gst_element_get_bus (pipeline);
  do {
    msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS |
        GST_MESSAGE_STATE_CHANGED);

    /* 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 (pipeline)) {
            GstState old_state, new_state, pending_state;
            gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
            g_print ("\nPipeline state changed from %s to %s:\n",
                gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
            /* Print the current capabilities of the sink element */
            print_pad_capabilities (sink, "sink");
          }
          break;
        default:
          /* We should not reach here because we only asked for ERRORs, EOS and STATE_CHANGED */
          g_printerr ("Unexpected message received.\n");
          break;
      }
      gst_message_unref (msg);
    }
  } while (!terminate);

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

gst_element_get_static_pad()获取always(static) pad,其他情况用:gst_element_foreach_pad()gst_element_iterate_pads()获取动态创建的Pad
gst_pad_get_current_caps()获取pad当前的caps,根据不同的element状态会有不同的结果,甚至可能不存在caps。
gst_pad_query_caps()获取当前可以支持的caps,当element处于NULL状态时,这个caps为Pad Template所支持的caps,其值可随状态变化而变化。

/* Create the element factories */
source_factory = gst_element_factory_find ("audiotestsrc");
sink_factory = gst_element_factory_find ("autoaudiosink");
if (!source_factory || !sink_factory) {
  g_printerr ("Not all element factories could be created.\n");
  return -1;
}

/* Print information about the pad templates of these factories */
print_pad_templates_information (source_factory);
print_pad_templates_information (sink_factory);

/* Ask the factories to instantiate actual elements */
source = gst_element_factory_create (source_factory, "source");
sink = gst_element_factory_create (sink_factory, "sink");

在使用gst_element_factory_make()接口创建element时,应用不需要关心element工厂。此处使用gst_element_factory_find()查找"audiotestsrc"工厂,再通过gst_element_factory_create()创建source element,gst_element_factory_make()是gst_element_factory_find() + gst_element_factory_create()的简化版

Pipeline的创建过程与其他示例相同,此例新增了状态变化的处理。

case GST_MESSAGE_STATE_CHANGED:
  /* We are only interested in state-changed messages from the pipeline */
  if (GST_MESSAGE_SRC (msg) == GST_OBJECT (pipeline)) {
    GstState old_state, new_state, pending_state;
    gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
    g_print ("\nPipeline state changed from %s to %s:\n",
        gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
    /* Print the current capabilities of the sink element */
    print_pad_capabilities (sink, "sink");
  }
  break;

因为我们在gst_bus_timed_pop_filtered()中加入了GST_MESSAGE_STATE_CHANGED,所以我们会收到状态变化的消息。在状态变化时,输出sink element的pad caps中当前状态的信息。

关于输出分析详见:分析

通过Pad进行数据传递

上面已经说了,Element间的数据的传输都是通过pad的,那么,究竟是如何进行数据传递的呢,下面我们来看看。pad具有两种模式,分别是PUSH和PULL。
PUSH模式,就是由上游element控制传输数据的大小与速度,将数据推送到下游element,所以下游的element一般都会设置一个缓冲区来接收数据,PUSH模式一般是通过gst_pad_push (GstPad * pad, GstBuffer * buffer)函数完成数据传递的操作;
PULL模式是由下游的element告诉上游element需要的数据量,PULL模式通过gst_pad_pull_range (GstPad * pad, guint64 offset, guint size, GstBuffer ** buffer)函数完成数据的获取。

详见link

一般sink pad都是永久型的,而element的src pad,则大多是随机型的,因为element本身知道它可以处理什么类型的数据,所以sinkpad是永久型的,而输出的数据,则是需要通过解析输入数据之后才知道输出,所以是随机型的。而element link,简单理解就是element的pad保存与之相连的pad信息,然后在传输数据的时候,通过之前link保存的信息而调用到下游element sinkpad,从而完成数据传递与信息交互。

动态连接Pipeline

前述案例中介绍了2种播放文件的方式:一种是在知道了文件的类型及编码方式后,手动创建所需Element并构造Pipeline;另一种是直接使用playbin,由playbin内部动态创建所需Element并连接Pipeline。很明显使用playbin的方式更加灵活,我们不需要在一开始就创建各种Pipeline,只需由playbin内部根据文件类型,自动构造Pipeline。
本节介绍:如何通过Pad事件动态的连接Pipeline,在本节的例子中,我们在将Pipeline设置为PLAYING状态之前,不会将所有的Element都连接起来,这种处理方式是可以的,但需要额外的处理。如果在设置PLAYING状态后不做任何操作,数据无法到达Sink,Pipeline会直接抛出一个错误并退出。如果在收到相应事件后,对其进行处理,并将Pipeline连接起来,Pipeline就可以正常工作。


我们常见的媒体,音频和视频都是通过某一种容器格式被包含中同一个文件中。播放时,我们需要将音视频数据分离出来,通常将具备这种功能的模块称为分离器(demuxer)。GStreamer针对常见的容器提供了相应的demuxer,如果一个容器文件中包含多种媒体数据(例如:一路视频,两路音频),这种情况下,demuxer会为些数据分别创建不同的Source Pad,每一个Source Pad可以被认为一个处理分支,可以创建多个分支分别处理相应的数据。

gst-launch-1.0 filesrc location=sintel_trailer-480p.ogv ! oggdemux name=demux ! queue ! vorbisdec ! autoaudiosink demux. ! queue ! theoradec ! videoconvert ! autovideosink

通过上面的命令播放文件时,会创建具有2个分支的Pipeline:

使用demuxer需要注意的一点是:demuxer只有在收到足够的数据时才能确定容器中包含哪些媒体信息,因此demuxer开始没有Source Pad,所以其他的Element无法在Pipeline创建时就连接到demuxer。
解决这种问题的办法是:在创建Pipeline时,我们只将Source Element到demuxer之间的Elements连接好,然后设置Pipeline状态为PLAYING,当demuxer收到足够的数据可以确定文件总包含哪些媒体流时,demuxer会创建相应的Source Pad,并通过事件告诉应用程序。我们可以通过监听demuxer的事件,在新的Source Pad被创建时,我们根据数据类型,创建相应的Element,再将其连接到Source Pad,形成完整的Pipeline。

为了简化逻辑,我们在本示例中会忽略视频的Source Pad,仅连接音频的Source Pad。

#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);
  // 注意,这里我们没有连接source与convert,是因为uridecode bin在Pipeline初始阶段还没有Source Pad。
  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;
  }
	/* 
由于uridecodebin可能会创建多个Pad,在每次有Pad被创建时,我们的回调函数都会被调用。
上面这段代码就是为了避免重复连接Pad。*/
  /* 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);
}

分析

/* 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");

首先创建了所需的Element:

  • uridecodebin会中内部实例化所需的Elements(source,demuxer,decoder)将URI所指向的媒体文件中的各种媒体数据分别提取出来。因为其包含了demuxer,所以Source Pad在初始化阶段无法访问,只有在收到相应事件后去动态连接Pad。
  • audioconvert用于在不同的音频数据格式之间进行转换。由于不同的声卡支持的数据类型不尽相同,所以在某些平台需要对音频数据类型进行转换。
  • autoaudiosink会自动查找声卡设备,并将音频数据传输到声卡上进行输出。
/* Connect to the pad-added signal */
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

GSignals在GStreamer中扮演着至关重要的角色。信号使你能在你所关心到事件发生后得到通知。在GLib中的信号通过信号名来进行识别,每个GObject对象都有其自己的信号。
在上面这行代码中,我们通过g_signal_connect将pad_added_handler回调连接到uridecodebin的“pad-added”信号上,同时附带回调函数的私有参数。GStreamer不会处理我们传入到data指针,只会将其作为参数传递给回调函数,这是传递私有数据给回调函数的常用方式。
一个GstElement可能会发出多个信号,可以使用gst-inspect工具查看具体到信号及参数。
在我们连接了“pad-added”的信号后,我们就可以将Pipeline的状态设置为PLAYING并按原有方式处理自己所关心到消息。
当Source Element收集到足够到信息,能产生数据时,它会创建Source Pad并且触发“pad-added”信号。这时,我们的回调函数就会被调用。

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

我们的回调函数是为了处理信号所携带到信息,所以必须用符合信号的数据类型,否则不能正确到处理相应数据。通过gst-inspect查看uridecodebin可以看到信号所需要到回调函数格式:

$ gst-inspect-1.0 uridecodebin
...
Element Signals:
  "pad-added" :  void user_function (GstElement* object,
                                     GstPad* arg0,
                                     gpointer user_data);
...             

Pipeline在我们将状态设置为PLAYING之前是不会进入播放状态,实际上PLAYING状态只是GStreamer状态中的一个,GStreamer总共包含4个状态:
1.NULL:NULL状态是所有Element被创建后的初始状态。
2.READY:READY状态表明GStreamer已经完成所需资源的检查,可以进入PAUSED状态。
3.PAUSED:Element处于暂停状态,表明其可以开始接收数据。Sink Element在接收了一个buffer后就会进入等待状态。
4.PLAYING:Element处于播放状态,时钟处于运行中,数据被依次处理。
GStreamer的状态必须按上面的顺序进行切换,例如:不能直接从NULL切换到PLAYING状态,NULL必须依次切换到READY,PAUSED后才能切换到PLAYING状态,当我们直接设置Pipeline的状态为PLAYING时,GStreamer内部会依次为我们切换到PLAYING状态。

播放时间控制

GStreamer提供了GstQuery的查询机制,用于查询Element或Pad的相应信息。例如:查询当前的播放速率,产生的延迟,是否支持跳转等。要查询所需的信息,首先需要构造一个查询的类型,然后使用Element或Pad的查询接口获取数据,最终再解析相应结果。
下面的例子介绍了如何使用GstQuery查询Pipeline的总时间:

   GstQuery *query = gst_query_new_duration (GST_FORMAT_TIME);
   gboolean res = gst_element_query (pipeline, query);
   if (res) {
     gint64 duration;
     gst_query_parse_duration (query, NULL, &amp;duration);
     g_print ("duration = %"GST_TIME_FORMAT, GST_TIME_ARGS (duration));
   } else {
     g_print ("duration query failed...");
   }
   gst_query_unref (query);

demo:在本示例中,我们通过查询Pipeline是否支持跳转(seeking),如果支持跳转(有些媒体不支持跳转,例如实时视频),我们会在播放10秒后跳转到其他位置。
在以前的示例中,我们在Pipeline开始执行后,只等待ERROR和EOS消息,然后退出。本例中,我们会在消息中设置等待超时时间,超时后,我们会去查询当前播放的时间,用于显示,这与播放器的进度条类似。

#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
  GstElement *playbin;  /* Our one and only element */
  gboolean playing;      /* Are we in the PLAYING state? */
  gboolean terminate;    /* Should we terminate execution? */
  gboolean seek_enabled; /* Is seeking enabled for this media? */
  gboolean seek_done;    /* Have we performed the seek already? */
  gint64 duration;       /* How long does this media last, in nanoseconds */
} CustomData;

/* Forward definition of the message processing function */
static void handle_message (CustomData *data, GstMessage *msg);

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

  data.playing = FALSE;
  data.terminate = FALSE;
  data.seek_enabled = FALSE;
  data.seek_done = FALSE;
  data.duration = GST_CLOCK_TIME_NONE;

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

  /* Create the elements */
  data.playbin = gst_element_factory_make ("playbin", "playbin");

  if (!data.playbin) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

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

  /* Start playing */
  ret = gst_element_set_state (data.playbin, 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.playbin);
    return -1;
  }

  /* Listen to the bus */
  bus = gst_element_get_bus (data.playbin);
  do {
    msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
        GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION_CHANGED);

    /* Parse message */
    if (msg != NULL) {
      handle_message (&data, msg);
    } else {
      /* We got no message, this means the timeout expired */
      if (data.playing) {
        gint64 current = -1;

        /* Query the current position of the stream */
        if (!gst_element_query_position (data.playbin, GST_FORMAT_TIME, &current)) {
          g_printerr ("Could not query current position.\n");
        }

        /* If we didn't know it yet, query the stream duration */
        if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
          if (!gst_element_query_duration (data.playbin, GST_FORMAT_TIME, &data.duration)) {
            g_printerr ("Could not query current duration.\n");
          }
        }

        /* Print current position and total duration */
        g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
            GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration));
		// 这里使用GST_TIME_FORMAT 和GST_TIME_ARGS 帮助我们方便地将GstClockTime的值转换为: ”时:分:秒“格式的字符串输出。
        /* If seeking is enabled, we have not done it yet, and the time is right, seek */
        if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
          g_print ("\nReached 10s, performing seek...\n");
          gst_element_seek_simple (data.playbin, GST_FORMAT_TIME,
              GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
          data.seek_done = TRUE;
        }
      }
    }
  } while (!data.terminate);

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

static void handle_message (CustomData *data, GstMessage *msg) {
  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);
      data->terminate = TRUE;
      break;
    case GST_MESSAGE_EOS:
      g_print ("End-Of-Stream reached.\n");
      data->terminate = TRUE;
      break;
    case GST_MESSAGE_DURATION_CHANGED:
      /* The duration has changed, mark the current one as invalid */
      data->duration = GST_CLOCK_TIME_NONE;
      break;
    case GST_MESSAGE_STATE_CHANGED: {
      GstState old_state, new_state, pending_state;
      gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
      if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->playbin)) {
        g_print ("Pipeline state changed from %s to %s:\n",
            gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));

        /* Remember whether we are in the PLAYING state or not */
        data->playing = (new_state == GST_STATE_PLAYING);

        if (data->playing) {
          /* We just moved to PLAYING. Check if seeking is possible */
          GstQuery *query;
          gint64 start, end;
          query = gst_query_new_seeking (GST_FORMAT_TIME);
          if (gst_element_query (data->playbin, query)) {
            gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
            if (data->seek_enabled) {
              g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
                  GST_TIME_ARGS (start), GST_TIME_ARGS (end));
            } else {
              g_print ("Seeking is DISABLED for this stream.\n");
            }
          }
          else {
            g_printerr ("Seeking query failed.");
          }
          gst_query_unref (query);
        }
      }
    } break;
    default:
      /* We should not reach here */
      g_printerr ("Unexpected message received.\n");
      break;
  }
  gst_message_unref (msg);
}

示例前部分内容与其他示例类似,构造Pipeline并使其进入PLAYING状态。之后开始监听Bus上的消息。与以前的示例相比,我们在gst_bus_timed_pop_filtered ()中加入了超时时间(100毫秒),这使得此函数如果在100毫秒内没有收到任何消息就会返回超时(msg == NULL),我们会在超时中去更新当前时间,如果返回相应消息(msg != NULL),我们在handle_message中处理相应消息。

GStreamer内部有统一的时间类型(GstClockTime),时间计算方式为:GstClockTime = 数值 x 时间单位。GStreamer提供了3种时间单位(宏定义):GST_SECOND(秒),GST_MSECOND(毫秒),GST_NSECOND(纳秒)。例如:
10秒: 10 * GST_SECOND
100毫秒:100 * GST_MSECOND
100纳秒:100 * GST_NSECOND

刷新播放时间

/* We got no message, this means the timeout expired */
if (data.playing) {
/* Query the current position of the stream */
if (!gst_element_query_position (data.pipeline, GST_FORMAT_TIME, &current)) {
  g_printerr ("Could not query current position.\n");
}
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
  if (!gst_element_query_duration (data.pipeline, GST_FORMAT_TIME, &data.duration)) {
     g_printerr ("Could not query current duration.\n");
  }
}

首先判断Pipeline的状态,仅在PLAYING状态时才更新当前时间,在非PLAYING状态时查询可能失败。这部分逻辑每秒大概会执行10次,频率足够用于界面的刷新。这里我们只将查询到的时间输出到终端。GstElement封装了相应的接口分别用于查询当前时间(gst_element_query_position)和总时间(gst_element_query_duration )。

/* If seeking is enabled, we have not done it yet, and the time is right, seek */
if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
  g_print ("\nReached 10s, performing seek...\n");
  gst_element_seek_simple (data.pipeline, GST_FORMAT_TIME,
      GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
  data.seek_done = TRUE;
}

我们同时在超时处理中判断是否需要进行seek操作(在播放到10s时,自动跳转到30s),这里我们直接在Pipeline对象上使用gst_element_seek_simple()来执行跳转操作。
gst_element_seek_simple所需要的参数为:

  • element : 需要执行seek操作的Element,这里是Pipeline。
  • format:执行seek的类型,这里使用GST_FORMAT_TIME表示我们基于时间的方式进行跳转。其他支持的类型可以查看 GstFormat。
  • seek_flags :通过标识指定seek的行为。 常用的标识如下,其他支持的flag详见GstSeekFlags。
    • GST_SEEK_FLAG_FLUSH:在执行seek前,清除Pipeline中所有buffer中缓存的数据。这可能导致Pipeline在填充的新数据被显示之前出现短暂的等待,但能提高应用更快的响应速度。如果不指定这个标志,Pipeline中的所有缓存数据会依次输出,然后才会播放跳转的位置,会导致一定的延迟。
    • GST_SEEK_FLAG_KEY_UNIT:对于大多数的视频,如果跳转的位置不是关键帧,需要依次解码该帧所依赖的帧(I帧及P帧)后,才能解码此非关键帧。使用这个标识后,seek会自动从最近的I帧开始播放。这个标识降低了seek的精度,提高了seek的效率。
    • GST_SEEK_FLAG_ACCURATE:一些媒体文件没有提供足够的索引信息,在这种文件中执行seek操作会非常耗时,针对这类文件,GStreamer通过内部计算得到需要跳转的位置,大部分的计算结果都是正确的。如果seek的位置不能达到所需精度时,可以增加此标识。但需要注意的是,使用此标识可能会导致seek耗费更多时间来寻找精确的位置。
  • seek_pos :需要跳转的位置,前面指定了seek的类型为时间,所以这里是30秒。

获取媒体元数据Metadata

GStream将元数据分为了两类:

流信息(Stream-info):用于描述流的属性。例如:编码类型,分辨率,采样率等。
Stream-info可以通过Pipeline中所有的GstCap获取,使用方式在媒体类型与Pad中有描述,本文将不再复述。

流标签(Stream-tag):用于描述非技术性的信息。例如:作者,标题,专辑等。
Stream-tag可以通过GstBus,监听GST_MESSAGE_TAG消息,从消息中提取相应信息。
需要注意的是,Gstreamer可能触发多次GST_MESSAGE_TAG消息,应用程序可以通过gst_tag_list_merge ()合并多个标签,再在适当的时间显示,当切换媒体文件时,需要清空缓存。
使用此函数时,需要采用GST_TAG_MERGE_PREPEND,这样后续更新的元数据会有更高的优先级。

#include <gst/gst.h>

static void
print_one_tag (const GstTagList * list, const gchar * tag, gpointer user_data)
{
  int i, num;

  num = gst_tag_list_get_tag_size (list, tag);
  for (i = 0; i < num; ++i) {
    const GValue *val;

    /* Note: when looking for specific tags, use the gst_tag_list_get_xyz() API,
     * we only use the GValue approach here because it is more generic */
    val = gst_tag_list_get_value_index (list, tag, i);
    if (G_VALUE_HOLDS_STRING (val)) {
      g_print ("\t%20s : %s\n", tag, g_value_get_string (val));
    } else if (G_VALUE_HOLDS_UINT (val)) {
      g_print ("\t%20s : %u\n", tag, g_value_get_uint (val));
    } else if (G_VALUE_HOLDS_DOUBLE (val)) {
      g_print ("\t%20s : %g\n", tag, g_value_get_double (val));
    } else if (G_VALUE_HOLDS_BOOLEAN (val)) {
      g_print ("\t%20s : %s\n", tag,
          (g_value_get_boolean (val)) ? "true" : "false");
    } else if (GST_VALUE_HOLDS_BUFFER (val)) {
      GstBuffer *buf = gst_value_get_buffer (val);
      guint buffer_size = gst_buffer_get_size (buf);

      g_print ("\t%20s : buffer of size %u\n", tag, buffer_size);
    } else if (GST_VALUE_HOLDS_DATE_TIME (val)) {
      GstDateTime *dt = g_value_get_boxed (val);
      gchar *dt_str = gst_date_time_to_iso8601_string (dt);

      g_print ("\t%20s : %s\n", tag, dt_str);
      g_free (dt_str);
    } else {
      g_print ("\t%20s : tag of type '%s'\n", tag, G_VALUE_TYPE_NAME (val));
    }
  }
}

static void
on_new_pad (GstElement * dec, GstPad * pad, GstElement * fakesink)
{
  GstPad *sinkpad;

  sinkpad = gst_element_get_static_pad (fakesink, "sink");
  if (!gst_pad_is_linked (sinkpad)) {
    if (gst_pad_link (pad, sinkpad) != GST_PAD_LINK_OK)
      g_error ("Failed to link pads!");
  }
  gst_object_unref (sinkpad);
}

int
main (int argc, char ** argv)
{
  GstElement *pipe, *dec, *sink;
  GstMessage *msg;
  gchar *uri;

  gst_init (&argc, &argv);

  if (argc < 2)
    g_error ("Usage: %s FILE or URI", argv[0]);

  if (gst_uri_is_valid (argv[1])) {
    uri = g_strdup (argv[1]);
  } else {
    uri = gst_filename_to_uri (argv[1], NULL);
  }

  pipe = gst_pipeline_new ("pipeline");

  dec = gst_element_factory_make ("uridecodebin", NULL);
  g_object_set (dec, "uri", uri, NULL);
  gst_bin_add (GST_BIN (pipe), dec);

  sink = gst_element_factory_make ("fakesink", NULL);
  gst_bin_add (GST_BIN (pipe), sink);

  g_signal_connect (dec, "pad-added", G_CALLBACK (on_new_pad), sink);

  gst_element_set_state (pipe, GST_STATE_PAUSED);

  while (TRUE) {
    GstTagList *tags = NULL;

    msg = gst_bus_timed_pop_filtered (GST_ELEMENT_BUS (pipe),
        GST_CLOCK_TIME_NONE,
        GST_MESSAGE_ASYNC_DONE | GST_MESSAGE_TAG | GST_MESSAGE_ERROR);

    if (GST_MESSAGE_TYPE (msg) != GST_MESSAGE_TAG) /* error or async_done */
      break;

    gst_message_parse_tag (msg, &tags);

    g_print ("Got tags from element %s:\n", GST_OBJECT_NAME (msg->src));
    gst_tag_list_foreach (tags, print_one_tag, NULL);
    g_print ("\n");
    gst_tag_list_unref (tags);

    gst_message_unref (msg);
  }

  if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
    GError *err = NULL;

    gst_message_parse_error (msg, &err, NULL);
    g_printerr ("Got error: %s\n", err->message);
    g_error_free (err);
  }

  gst_message_unref (msg);
  gst_element_set_state (pipe, GST_STATE_NULL);
  gst_object_unref (pipe);
  g_free (uri);
  return 0;
}

多线程

GStreamer框架会自动处理多线程的逻辑,但在某些情况下,我们仍然需要根据实际的情况自己将部分Pipeline在单独的线程中执行,本文将介绍如何处理这种情况。
GStreamer框架是一个支持多线程的框架,线程会根据Pipeline的需要自动创建和销毁,例如,将媒体流与应用线程解耦,应用线程不会被GStreamer的处理阻塞。而且,GStreamer的插件还可以创建自己所需的线程用于媒体的处理,例如:在一个4核的CPU上,视频解码插件可以创建4个线程来最大化利用CPU资源。
在创建Pipeline时,我们还可以指定某个Pipeline的分支在不同的线程中执行(例如,使audio、video同时在不同的线程中进行解码)。这是通过queue Element来实现的,queue的sink pad仅仅将数据放入队列,另外一个线程从队列中取出数据,并传递到下一个Element。queue通常也被用于作为数据缓冲,缓冲区大小可以通过queue的属性进行配置。

在上面的示例Pipeline中,souce是audiotestsrc,会产生一个相应的audio信号,然后使用tee Element将数据分为两路,一路被用于播放,通过声卡输出,另一路被用于转换为视频波形,用于输出到屏幕。
示例图中的红色阴影部分表示位于同一个线程中,queue会创建单独的线程,所以上面的Pipeline使用了3个线程完成相应的功能。拥有多个sink的Pipeline通常需要多个线程,因为在多个sync间进行同步的时候,sink会阻塞当前所在线程直到所等待的事件发生。

#include <gst/gst.h>

int main(int argc, char *argv[]) {
  GstElement *pipeline, *audio_source, *tee, *audio_queue, *audio_convert, *audio_resample, *audio_sink;
  GstElement *video_queue, *visual, *video_convert, *video_sink;
  GstBus *bus;
  GstMessage *msg;
  GstPad *tee_audio_pad, *tee_video_pad;
  GstPad *queue_audio_pad, *queue_video_pad;

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

  /* Create the elements 
	首先创建所需的Element:audiotestsrc会产生测试的音频波形数据。wavescope 会将输入的音频数据转换为波形图像。audioconvert,audioresample,videoconvert保证了Pipeline中各个Element之间的数据可以互相兼容
	使得Pipeline能够被正确的link起来,如果不需要对数据进行转换,这些Element会直接将数据发送到下一个Element,这种情况下的性能影响可以忽略不计。
*/
  audio_source = gst_element_factory_make ("audiotestsrc", "audio_source");
  tee = gst_element_factory_make ("tee", "tee");
  audio_queue = gst_element_factory_make ("queue", "audio_queue");
  audio_convert = gst_element_factory_make ("audioconvert", "audio_convert");
  audio_resample = gst_element_factory_make ("audioresample", "audio_resample");
  audio_sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
  video_queue = gst_element_factory_make ("queue", "video_queue");
  visual = gst_element_factory_make ("wavescope", "visual");
  video_convert = gst_element_factory_make ("videoconvert", "csp");
  video_sink = gst_element_factory_make ("autovideosink", "video_sink");

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

  if (!pipeline || !audio_source || !tee || !audio_queue || !audio_convert || !audio_resample || !audio_sink ||
      !video_queue || !visual || !video_convert || !video_sink) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

  /* Configure elements */
  g_object_set (audio_source, "freq", 215.0f, NULL);	// “freq”会设置audiotestsrc输出波形的频率为215Hz
  g_object_set (visual, "shader", 0, "style", 1, NULL); // 设置“shader”和“style”使得波形更加连续

  /* Link all elements that can be automatically linked because they have "Always" pads */
  gst_bin_add_many (GST_BIN (pipeline), audio_source, tee, audio_queue, audio_convert, audio_resample, audio_sink,
      video_queue, visual, video_convert, video_sink, NULL);
  if (gst_element_link_many (audio_source, tee, NULL) != TRUE ||
      gst_element_link_many (audio_queue, audio_convert, audio_resample, audio_sink, NULL) != TRUE ||
      gst_element_link_many (video_queue, visual, video_convert, video_sink, NULL) != TRUE) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (pipeline);
    return -1;
  }

  /* Manually link the Tee, which has "Request" pads */
  tee_audio_pad = gst_element_get_request_pad (tee, "src_%u");
  g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
  queue_audio_pad = gst_element_get_static_pad (audio_queue, "sink");
  tee_video_pad = gst_element_get_request_pad (tee, "src_%u");
  g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
  queue_video_pad = gst_element_get_static_pad (video_queue, "sink");
  if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
      gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK) {
    g_printerr ("Tee could not be linked.\n");
    gst_object_unref (pipeline);
    return -1;
  }
  gst_object_unref (queue_audio_pad);
  gst_object_unref (queue_video_pad);

  /* Start playing the pipeline */
  gst_element_set_state (pipeline, GST_STATE_PLAYING);

  /* Wait until error or EOS */
  bus = gst_element_get_bus (pipeline);
  msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

  /* Release the request pads from the Tee, and unref them */
  gst_element_release_request_pad (tee, tee_audio_pad);
  gst_element_release_request_pad (tee, tee_video_pad);
  gst_object_unref (tee_audio_pad);
  gst_object_unref (tee_video_pad);

  /* Free resources */
  if (msg != NULL)
    gst_message_unref (msg);
  gst_object_unref (bus);
  gst_element_set_state (pipeline, GST_STATE_NULL);

  gst_object_unref (pipeline);
  return 0;
}
gst_bin_add_many (GST_BIN (pipeline), audio_source, tee, audio_queue, audio_convert, audio_sink,
    video_queue, visual, video_convert, video_sink, NULL);
if (gst_element_link_many (audio_source, tee, NULL) != TRUE ||
    gst_element_link_many (audio_queue, audio_convert, audio_sink, NULL) != TRUE ||
    gst_element_link_many (video_queue, visual, video_convert, video_sink, NULL) != TRUE) {
  g_printerr ("Elements could not be linked.\n");
  gst_object_unref (pipeline);
  return -1;
}

这里我们使用gst_element_link_many 将多个Element连接起来,需要注意的是,这里我们只连接了拥有Always Pad的Eelement。虽然gst_element_link_many() 能够在内部处理Request Pad的情况,但我们仍然需要单独释放Request Pad,如果直接使用此函数连接所有的Element,这样容易忘记释放Request Pad。因此我们使用下面的代码单独处理Request Pad。

/* Manually link the Tee, which has "Request" pads */
tee_audio_pad = gst_element_get_request_pad (tee, "src_%u");
g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
queue_audio_pad = gst_element_get_static_pad (audio_queue, "sink");
tee_video_pad = gst_element_get_request_pad (tee, "src_%u");
g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
queue_video_pad = gst_element_get_static_pad (video_queue, "sink");
if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
    gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK) {
  g_printerr ("Tee could not be linked.\n");
  gst_object_unref (pipeline);
  return -1;
}
gst_object_unref (queue_audio_pad);
gst_object_unref (queue_video_pad);

为了能够连接到Request Pad,我们需要主动的向Element取得相应的Pad。由于一个Element可以提供不同的Request Pad,所以我们需要指定所需的“Pad Template”,Element提供的Pad Template可以通过gst-inspect查看。从下面的结果可以发现,tee提供了2种类型的模板, ”sink“ 和“src_%u"。

$ gst-inspect-1.0  tee
...
Pad Templates:
  SRC template: 'src_%u'
    Availability: On request
      Has request_new_pad() function: gst_tee_request_new_pad
    Capabilities:
      ANY

  SINK template: 'sink'
    Availability: Always
    Capabilities:
      ANY
...

由于我们这里需要的是2个Source Pad,所以我们通过gst_element_get_request_pad (tee, “src_%u”)获取两个Request Pad分别用于audio和video。queue的Sink Pad是Alwasy Pad,所以我们直接使用gst_element_get_static_pad 获取其Sink Pad。最后再通过gst_pad_link()将其连接起来,在gst_element_link()和gst_element_link_many()内部也是使用此函数连接两个Element的Pad。

需要注意的是,我们通过Element获取到的Pad的引用计数会自动增加,因此我们需要调用gst_object_unref()释放相关的引用,对于Request Pad,我们需要在Pipeline执行完成后进行释放。

gst_element_release_request_pad (tee, tee_audio_pad);
gst_element_release_request_pad (tee, tee_video_pad);
gst_object_unref (tee_audio_pad);
gst_object_unref (tee_video_pad);

除了播放完成后正常的资源释放外,我们还要对Request进行释放,需要首先调用gst_element_release_request_pad(),最后再释放相应的对象。

与QT集成

通常我们的播放引擎需要和GUI进行集成,在使用GStreamer时,GStreamer会负责媒体的播放及控制,GUI会负责处理用户的交互操作以及创建显示的窗口。本例中我们将结合QT介绍如何指定GStreamer将视频输出到指定窗口,以及如何利用GStreamer上报的信息去更新GUI。
与GUI集成有两个方面需要注意:

  • 显示窗口的管理
    显示窗口通常由GUI框架创建,所以我们需要将具体的窗口信息告诉GStreamer。由于各个平台使用不同的方式传递窗口句柄,GStreamer提供了一个抽象接口(GstVideoOverlay),用于屏蔽平台的差异,我们可以直接将GUI创建的窗口ID传递给GStreamer。
  • GUI界面的更新
    大多数GUI框架都需要在主线程中去做UI的刷新操作,但GStreamer内部可能会创建多个线程,这就需要通过GstBus及GUI自带的通信机制将所有GStreamer产生的消息传递到GUI主线程,再由GUI主线程对界面进行刷新。

demo:
qtoverlay.h

#ifndef _QTOVERLAY_
#define _QTOVERLAY_

#include <gst/gst.h>

#include <QWidget>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QSlider>
#include <QTimer>

class PlayerWindow : public QWidget
{
    Q_OBJECT
public:
  PlayerWindow(GstElement *p);

  WId getVideoWId() const ;
  static gboolean postGstMessage(GstBus * bus, GstMessage * message, gpointer user_data);

private slots:
  void onPlayClicked() ;
  void onPauseClicked() ;
  void onStopClicked() ;
  void onAlbumAvaiable(const QString &album);
  void onState(GstState st);
  void refreshSlider();
  void onSeek();
  void onEos();

signals:
  void sigAlbum(const QString &album);
  void sigState(GstState st);
  void sigEos();
  
private:
  GstElement *pipeline;
  QPushButton *playBt;
  QPushButton *pauseBt;
  QPushButton *stopBt;
  QWidget *videoWindow;
  QSlider *slider;
  QHBoxLayout *buttonLayout;
  QVBoxLayout *playerLayout;
  QTimer *timer;

  GstState state;
  gint64 totalDuration;
};

#endif

qtoverlay.cpp

#include <gst/video/videooverlay.h>
#include <QApplication>
#include "qtoverlay.h"

PlayerWindow::PlayerWindow(GstElement *p)
    :pipeline(p)
    ,state(GST_STATE_NULL)
    ,totalDuration(GST_CLOCK_TIME_NONE)
{
    playBt = new QPushButton("Play");
    pauseBt = new QPushButton("Pause");
    stopBt = new QPushButton("Stop");
    videoWindow = new QWidget();
    slider = new QSlider(Qt::Horizontal);
    timer = new QTimer();

    connect(playBt, SIGNAL(clicked()), this, SLOT(onPlayClicked()));
    connect(pauseBt, SIGNAL(clicked()), this, SLOT(onPauseClicked()));
    connect(stopBt, SIGNAL(clicked()), this, SLOT(onStopClicked()));
    connect(slider, SIGNAL(sliderReleased()), this, SLOT(onSeek()));

    buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(playBt);
    buttonLayout->addWidget(pauseBt);
    buttonLayout->addWidget(stopBt);
    buttonLayout->addWidget(slider);

    playerLayout = new QVBoxLayout;
    playerLayout->addWidget(videoWindow);
    playerLayout->addLayout(buttonLayout);

    this->setLayout(playerLayout);

    connect(timer, SIGNAL(timeout()), this, SLOT(refreshSlider()));
    connect(this, SIGNAL(sigAlbum(QString)), this, SLOT(onAlbumAvaiable(QString)));
    connect(this, SIGNAL(sigState(GstState)), this, SLOT(onState(GstState)));
    connect(this, SIGNAL(sigEos()), this, SLOT(onEos()));
}

WId PlayerWindow::getVideoWId() const {
    return videoWindow->winId();
}

void PlayerWindow::onPlayClicked() {
    GstState st = GST_STATE_NULL;
    gst_element_get_state (pipeline, &st, NULL, GST_CLOCK_TIME_NONE);
    if (st < GST_STATE_PAUSED) {
        // Pipeline stopped, we need set overlay again
        GstElement *vsink = gst_element_factory_make ("ximagesink", "vsink");
        g_object_set(GST_OBJECT(pipeline), "video-sink", vsink, NULL);
        WId xwinid = getVideoWId();
        gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (vsink), xwinid);
    }
    gst_element_set_state (pipeline, GST_STATE_PLAYING);
}

void PlayerWindow::onPauseClicked() {
    gst_element_set_state (pipeline, GST_STATE_PAUSED);
}

void PlayerWindow::onStopClicked() {
    gst_element_set_state (pipeline, GST_STATE_NULL);
}

void PlayerWindow::onAlbumAvaiable(const QString &album) {
    setWindowTitle(album);
}

void PlayerWindow::onState(GstState st) {
    if (state != st) {
        state = st;
        if (state == GST_STATE_PLAYING){
            timer->start(1000);
        }
        if (state < GST_STATE_PAUSED){
            timer->stop();
        }
    }
}

void PlayerWindow::refreshSlider() {
    gint64 current = GST_CLOCK_TIME_NONE;
    if (state == GST_STATE_PLAYING) {
        if (!GST_CLOCK_TIME_IS_VALID(totalDuration)) {
            if (gst_element_query_duration (pipeline, GST_FORMAT_TIME, &totalDuration)) {
                slider->setRange(0, totalDuration/GST_SECOND);
            }
        }
        if (gst_element_query_position (pipeline, GST_FORMAT_TIME, &current)) {
            g_print("%ld / %ld\n", current/GST_SECOND, totalDuration/GST_SECOND);
            slider->setValue(current/GST_SECOND);
        }
    }
}

void PlayerWindow::onSeek() {
    gint64 pos = slider->sliderPosition();
    g_print("seek: %ld\n", pos);
    gst_element_seek_simple (pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH ,
                  pos * GST_SECOND);
}

void PlayerWindow::onEos() {
    gst_element_set_state (pipeline, GST_STATE_NULL);
}

gboolean PlayerWindow::postGstMessage(GstBus * bus, GstMessage * message, gpointer user_data) {
    PlayerWindow *pw = NULL;
    if (user_data) {
        pw = reinterpret_cast<PlayerWindow*>(user_data);
    }
    switch (GST_MESSAGE_TYPE(message)) {
        case GST_MESSAGE_STATE_CHANGED: {
            GstState old_state, new_state, pending_state;
            gst_message_parse_state_changed (message, &old_state, &new_state, &pending_state);
            pw->sigState(new_state);
            break;
        }
        case GST_MESSAGE_TAG: {
            GstTagList *tags = NULL;
            gst_message_parse_tag(message, &tags);
            gchar *album= NULL;
            if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &album)) {
                pw->sigAlbum(album);
                g_free(album);
            }
            gst_tag_list_unref(tags);
            break;
        }
        case GST_MESSAGE_EOS: {
            pw->sigEos();
            break;
        }
        default:
            break;
    }
    return TRUE;
}

int main(int argc, char *argv[])
{
  gst_init (&argc, &argv);
  QApplication app(argc, argv);
  app.connect(&app, SIGNAL(lastWindowClosed()), &app, SLOT(quit ()));

  // prepare the pipeline
  GstElement *pipeline = gst_parse_launch ("playbin uri=file:///home/john/video/sintel_trailer-480p.webm", NULL);

  // prepare the ui
  PlayerWindow *window = new PlayerWindow(pipeline);
  window->resize(900, 600);
  window->show();

  // seg window id to gstreamer
  GstElement *vsink = gst_element_factory_make ("ximagesink", "vsink");
  WId xwinid = window->getVideoWId();
  gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (vsink), xwinid);
  g_object_set(GST_OBJECT(pipeline), "video-sink", vsink, NULL);

  // connect to interesting signals
  GstBus *bus = gst_element_get_bus(pipeline);
  gst_bus_add_watch(bus, &PlayerWindow::postGstMessage, window);
  gst_object_unref(bus);

  // run the pipeline
  GstStateChangeReturn sret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
  if (sret == GST_STATE_CHANGE_FAILURE) {
    gst_element_set_state (pipeline, GST_STATE_NULL);
    gst_object_unref (pipeline);
    // Exit application
    QTimer::singleShot(0, QApplication::activeWindow(), SLOT(quit()));
  }

  int ret = app.exec();

  window->hide();
  gst_element_set_state (pipeline, GST_STATE_NULL);
  gst_object_unref (pipeline);

  return ret;
}

qtoverlay.pro

QT += core gui widgets
TARGET = qtoverlay

INCLUDEPATH += /usr/include/glib-2.0
INCLUDEPATH += /usr/lib/x86_64-linux-gnu/glib-2.0/include
INCLUDEPATH += /usr/include/gstreamer-1.0
INCLUDEPATH += /usr/lib/x86_64-linux-gnu/gstreamer-1.0/include
LIBS += -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lgstvideo-1.0

SOURCES += qtoverlay.cpp
HEADERS += qtoverlay.h
 // prepare the pipeline
  GstElement *pipeline = gst_parse_launch ("playbin uri=file:///home/jleng/video/sintel_trailer-480p.webm", NULL);

  // prepare the ui
  PlayerWindow *window = new PlayerWindow(pipeline);
  window->resize(900, 600);
  window->show();

在main函数中对GStreamer进行初始化及创建了QT的应用对象后,构造了Pipline,构造GUI窗口对象。在PlayerWindow的构造函数中初始化按钮及窗口,同时创建定时刷新进度条的Timer。

// seg window id to gstreamer
  GstElement *vsink = gst_element_factory_make ("ximagesink", "vsink");
  WId xwinid = window->getVideoWId();
  gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (vsink), xwinid);
  g_object_set(GST_OBJECT(pipeline), "video-sink", vsink, NULL);
  ...
  gst_bus_add_watch(bus, &PlayerWindow::postGstMessage, window);
  ...
  GstStateChangeReturn sret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
  ...
  int ret = app.exec();
  ...

接着我们单独创建了ximagesink用于视频渲染,同时我们将Qt创建的视频窗口ID设置给GStreamer,让GStreamer得到渲染的窗口ID,接着使用g_object_set()将自定义的Sink通过“video-sink”属性设置到playbin中。
同时,我们设置了GStreamer的消息处理函数,所有的消息都会在postGstMessage函数中被转发。为了后续调用GUI对象中的接口,我们需要将GUI窗口指针作为user-data,在postGstMessage中再转换为GUI对象。
接着设置Pipeline的状态为PLAYING开始播放。
最后调用GUI框架的事件循环,exec()会一直执行,直到关闭窗口。
由于GStreamer的GstBus会默认使用GLib的主循环及事件处理机制,所以必须要保证GLib默认的MainLoop在某个线程中运行。在本例中,Qt在Linux下会自动使用GLib的主循环,所以我们无需额外进行处理。

gboolean PlayerWindow::postGstMessage(GstBus * bus, GstMessage * message, gpointer user_data) {
    PlayerWindow *pw = NULL;
    if (user_data) {
        pw = reinterpret_cast<PlayerWindow*>(user_data);
    }
    switch (GST_MESSAGE_TYPE(message)) {
        case GST_MESSAGE_STATE_CHANGED: {
            GstState old_state, new_state, pending_state;
            gst_message_parse_state_changed (message, &old_state, &new_state, &pending_state);
            pw->sigState(new_state);
            break;
        }
        case GST_MESSAGE_TAG: {
            GstTagList *tags = NULL;
            gst_message_parse_tag(message, &tags);
            gchar *album= NULL;
            if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &album)) {
                pw->sigAlbum(album);
                g_free(album);
            }
            gst_tag_list_unref(tags);
            break;
        }
        case GST_MESSAGE_EOS: {
            pw->sigEos();
            break;
        }
        default:
            break;
    }
    return TRUE;
}

在转换后GUI对象后,再根据消息类型进行处理。在postGstMessage中我们没有直接更新GUI,因为GStreamer的Bus处理线程与GUI主线程可能为不同线程,直接更新GUI会出错或无效。因此利用Qt的signal-slot机制在相应的槽函数中就行GUI信息的更新。这里只处理了3种消息STATE_CHANGED(状态变化),TAG(媒体元数据及编码信息),EOS(播放结束),GStreamer所支持的消息可查看官方文档

void PlayerWindow::onPlayClicked() {
    GstState st = GST_STATE_NULL;
    gst_element_get_state (pipeline, &st, NULL, GST_CLOCK_TIME_NONE);
    if (st < GST_STATE_PAUSED) {
        // Pipeline stopped, we need set overlay again
        GstElement *vsink = gst_element_factory_make ("ximagesink", "vsink");
        g_object_set(GST_OBJECT(pipeline), "video-sink", vsink, NULL);
        WId xwinid = getVideoWId();
        gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (vsink), xwinid);
    }
    gst_element_set_state (pipeline, GST_STATE_PLAYING);
}

当点击Play按钮时,onPlayClicked函数会被调用,我们在此直接调用GStreamer的接口gst_element_set_state设置Pipeline的状态。当播放结束或点击Stop时,GStreamer会在状态切换到NULL时释放所有资源,所以我们在此需要重新设置playbin的video-sink,并指定视频输出窗口。
Pause,Stop的处理类似,直接调用gst_element_set_state ()将Pipeline设置为相应状态。

void PlayerWindow::refreshSlider() {
    gint64 current = GST_CLOCK_TIME_NONE;
    if (state == GST_STATE_PLAYING) {
        if (!GST_CLOCK_TIME_IS_VALID(totalDuration)) {
            if (gst_element_query_duration (pipeline, GST_FORMAT_TIME, &totalDuration)) {
                slider->setRange(0, totalDuration/GST_SECOND);
            }
        }
        if (gst_element_query_position (pipeline, GST_FORMAT_TIME, &current)) {
            g_print("%ld / %ld\n", current/GST_SECOND, totalDuration/GST_SECOND);
            slider->setValue(current/GST_SECOND);
        }
    }
}

void PlayerWindow::onSeek() {
    gint64 pos = slider->sliderPosition();
    g_print("seek: %ld\n", pos);
    gst_element_seek_simple (pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH ,
                  pos * GST_SECOND);
}

在构造函数中创建了Timer用于每秒刷新进度条,在refreshSlider被调用时,我们通过gst_element_query_duration() 和gst_element_query_position ()得到文件的总时间和当前时间,并刷新进度条。由于GStreamer返回时间单位为纳秒,所以我们需要通过GST_SECOND将其转换为秒用于时间显示。
我们同样处理了用户的Seek操作,在拉动进度条到某个位置时,获取Seek的位置,调用gst_element_seek_simple ()跳转到指定位置。我们不用关心对GStreamer的调用是处于哪个线程,GStreamer内部会自动进行处理。

Reference

  1. https://www.cnblogs.com/xleng/p/10948838.html
  2. https://www.cnblogs.com/xleng/p/11771328.html
  3. https://www.cnblogs.com/xleng/p/11008239.html
  4. https://gstreamer.freedesktop.org/documentation/tutorials/basic/hello-world.html
  5. https://gstreamer.freedesktop.org/documentation/installing/on-linux.html
  6. https://www.cnblogs.com/xleng/p/11039519.html
  7. https://www.cnblogs.com/xleng/p/11113405.html
  8. https://www.cnblogs.com/xleng/p/11194226.html
  9. https://www.cnblogs.com/xleng/p/11608486.html
  10. https://blog.csdn.net/weixin_41944449/article/details/81568845
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐