在音视频领域接触最多实现的方案通常是通过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"); // 创建Pipelineif (!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: AlwaysCapabilities:audio/x-rawformat: S16LElayout: interleavedrate: [ 1, 48000 ]channels: [ 1, 2 ]audio/x-ac3framed: 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: AlwaysCapabilities:video/x-rawformat: { 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-bayerformat: { 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: AlwaysCapabilities:video/quicktimevideo/mj2audio/x-m4aapplication/x-3gpSRC template: 'video_%u'Availability: SometimesCapabilities:ANYSRC template: 'audio_%u'Availability: SometimesCapabilities:ANYSRC template: 'subtitle_%u'Availability: SometimesCapabilities: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 requestHas request_new_pad() function: gst_tee_request_new_padCapabilities: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);elseg_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");elseg_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输出波形的频率为215Hzg_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 requestHas request_new_pad() function: gst_tee_request_new_padCapabilities:ANYSINK template: 'sink'Availability: AlwaysCapabilities: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 againGstElement *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 pipelineGstElement *pipeline = gst_parse_launch ("playbin uri=file:///home/john/video/sintel_trailer-480p.webm", NULL);// prepare the uiPlayerWindow *window = new PlayerWindow(pipeline);window->resize(900, 600);window->show();// seg window id to gstreamerGstElement *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 signalsGstBus *bus = gst_element_get_bus(pipeline);gst_bus_add_watch(bus, &PlayerWindow::postGstMessage, window);gst_object_unref(bus);// run the pipelineGstStateChangeReturn 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 applicationQTimer::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 = qtoverlayINCLUDEPATH += /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.0SOURCES += qtoverlay.cpp
HEADERS += qtoverlay.h
 // prepare the pipelineGstElement *pipeline = gst_parse_launch ("playbin uri=file:///home/jleng/video/sintel_trailer-480p.webm", NULL);// prepare the uiPlayerWindow *window = new PlayerWindow(pipeline);window->resize(900, 600);window->show();

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

// seg window id to gstreamerGstElement *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 againGstElement *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

QT 音视频开发 基于gstreamer框架相关推荐

  1. Qt音视频开发27-Onvif设备搜索

    一.前言 最近业余时间主要研究音视频开发这块,前面的文章写了好多种视频监控内核,一旦将这些内核搞定以后,视频监控的相关功能水到渠成.做视频监控系统,绕不过onvif这玩意,这玩意主要就是为了统一一个大 ...

  2. Qt音视频开发06-海康sdk内核linux客户端

    一.前言 海康sdk的示例在官方是提供了的,但是无论UI还是交互简直是宇宙无敌的垃圾,猜测应该是初学者编写的,估计练手用的,所以老早就想把这个linux支持集成到自己的示例中,既然已经支持了windo ...

  3. Qt音视频开发01-共享解码线程(耗时一年/性能凶残/至臻完美)

    一.前言 大概在8年前就开始用ffmpeg做视频解码的显示,第一个版本就100行代码左右,功能极其简单,就是开个线程解码视频流转成图片发给主界面绘制.时间过得真快,从当初的一胎到现在二胎都上学了三胎计 ...

  4. Qt音视频开发02-海康sdk解码(支持句柄/回调/GPU模式/支持win/linux)

    一.前言 为何还要选用使用海康sdk,之前不是ffmpeg已经牛皮吹上天了吗?这个问题问得好,那是因为无论ffmpeg也好还是vlc/mpv之类的,都是实现的播放相关,不同的监控硬件厂家对应设备还有很 ...

  5. Qt音视频开发26-ffmpeg播放器

    一.前言 用ffmpeg来实现自己的播放器,这是一直以来的一个目标,之前的难点卡在音视频同步以及如何播放声音这两点(尽管之前已经进行过不少的尝试和探索,但是问题还是挺多,比如音视频同步不完美,有些文件 ...

  6. Qt音视频开发22-音频播放QAudioOutput

    一.前言 以前一直以为只有Qt5以后才有QAudioOutput播放音频,其实从Qt4.6开始就有,在Qt6中变成了QAudioSink,功能一样.用QAudioOutput播放音频pcm数据极其方便 ...

  7. Qt音视频开发24-ffmpeg音视频同步

    一.前言 用ffmpeg来做音视频同步,个人认为这个是ffmpeg基础处理中最难的一个,无数人就卡在这里,怎么也不准,本人也是尝试过网上各种demo,基本上都是渣渣,要么仅仅支持极其少量的视频文件比如 ...

  8. Qt音视频开发22-通用GPU显示

    一.前言 采用GPU来绘制实时视频一直以来都是个难点,如果是安防行业的做视频监控开发这块的人员,这个坎必须迈过去,本人一直从事的是安防行业的电子围栏这个相当小众的细分市场的开发,视频监控这块仅仅是周边 ...

  9. Qt音视频开发12-mpv解码播放

    一.前言 之前玩了vlc解码和ffmpeg解码,前阵子有个客户需要换成mpv解码,于是研究了下mpv的使用方法,自从用了mpv以后发现爱不释手,这玩意天生适合极客和程序员啊,居然将各种处理封装成了命令 ...

最新文章

  1. APACHE利用Limit模块限制IP连接数
  2. 无数种求逆元的方法总结
  3. 从零开始学习音视频编程技术(四) FFMPEG的使用
  4. 进出仓原理_通达信浪口主图+窥窃天机副图+进出仓副图强强组合
  5. cnblogs今天开通了!
  6. php图片转化为base64,php获取图片转为base64
  7. winhex查看mysql_使用WINHEX查看innodb的BTree高度
  8. Ruff 将助力广东金融高新区“区块链+”金融科技创新与应用落地
  9. 串行通信又称为点对点通信(PtP),其中点对点如何理解
  10. linux 内核函数 filp_open、filp_read、IS_ERR、ERR_PTR、PTR_ERR 简介
  11. uiautomatorviewer 简单使用
  12. Java爆笑梗,jvav是什么鬼!盘点那些迷你小学生中那些笑死人的梗
  13. matlab contour3函数,matlab 等值线函数 contour
  14. 微信用户授权及企业付款
  15. mac配置git公钥
  16. POJ 2159 分组密码与流密码
  17. 三次B样条曲线拟合算法
  18. 《ClickHouse原理解析与应用实践》读书笔记(1)
  19. 长白山项目开发小组,day1
  20. 链表应用之多项式相加

热门文章

  1. 常用数据库文件扩展名
  2. 股票接口实时交易数据怎么查询?
  3. Java项目:SSM企业门户网站
  4. STM32 ADC 引脚约0.7V问题解决方法
  5. 无线血糖仪行业调研报告 - 市场现状分析与发展前景预测
  6. Python-Django毕业设计宾馆管理信息系统(程序+Lw)
  7. 韩服服务器稳定吗,韩服体验最糟糕?论中美日韩四个服务器的游戏体验
  8. 2013年第一季度 网游格局
  9. 怎样用计算机打出分数,分数怎么打出来
  10. windows实现支付宝在线支付 python