摘要

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

与GUI集成

我们知道与GUI集成有两个方面需要注意:

  • 显示窗口的管理。

  由于显示窗口通常由GUI框架创建,所以我们需要将具体的窗口信息告诉GStreamer。由于各个平台使用不同的方式传递窗口句柄,GStreamer提供了一个抽象接口(GstVideoOverlay),用于屏蔽平台的差异,我们可以直接将GUI创建的窗口ID传递给GStreamer。

  • GUI界面的更新

  大多数GUI框架都需要在主线程中去做UI的刷新操作,但GStreamer内部可能会创建多个线程,这就需要通过GstBus及GUI自带的通信机制将所有GStreamer产生的消息传递到GUI主线程,再由GUI主线程对界面进行刷新。

  下面我们将以QT为例来了解如何处理GStreamer与GUI框架的集成。

示例代码

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();
}
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(, 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(, );
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(, 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

分别保存以上内容到各个文件,执行下列命令即可得到可执行程序。如果找不到头文件及库文件,需要根据实际路径修改qtoverlay.pro文件中的内容。

qmake -o Makefile qtoverlay.pro
make

源码分析

  // 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(, );
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的主循环及事件处理机制,所以必须要保证GLi默认的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所支持的消息可查看官方文档GstMessage

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的接口设置Pipeline的状态。当播放结束或点击Stop时,GStreamer会在状态切换到NULL时释放所有资源,所以我们在此需要重新设置playbin的vido-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(, 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内部会自动进行处理。

总结

通过本文,我们学习到:

  • 如何使用gst_video_overlay_set_window_handle ()将GUI的窗口句柄传递给GStremaer。
  • 如何使用信号槽传递消息到GUI主线程。
  • 如何使用Timer定时刷新GUI。

引用

https://gstreamer.freedesktop.org/documentation/video/gstvideooverlay.html?gi-language=c
https://gstreamer.freedesktop.org/documentation/tutorials/basic/toolkit-integration.html?gi-language=c
https://doc.qt.io/qt-5/qmake-manual.html

作者:John.Leng
本文版权归作者所有,欢迎转载。商业转载请联系作者获得授权,非商业转载请在文章页面明显位置给出原文连接.

GStreamer基础教程11 - 与QT集成的更多相关文章

  1. 【GStreamer开发】GStreamer基础教程11——调试工具

    目标 有时我们的应用并没有按照我们的预期来工作,并且在总线上获得的错误信息也没有足够的内容.这时我们该怎么办呢?幸运的时,GStreamer自身提供了大量的调试信息,通常这些信息会给出一些线索,指向出 ...

  2. 【GStreamer开发】GStreamer基础教程05——集成GUI工具

    目标 本教程展示了如何在GStreamer集成一个GUI(比如:GTK+).最基本的原则是GStreamer处理多媒体的播放而GUI处理和用户的交互. 在这个教程里面,我们可以学到: 如何告诉GStr ...

  3. GStreamer基础教程12 - 常用命令工具

    摘要 GStreamer提供了不同的命令行工具用于快速的查看信息以及验证Pipeline的是否能够正确运行,在平时的开发过程中,我们也优先使用GStreamer的命令行工具验证,再将Pipeline集 ...

  4. 【GStreamer开发】GStreamer基础教程14——常用的element

    目标 本教程给出了一系列开发中常用的element.它们包括大杂烩般的eleemnt(比如playbin2)以及一些调试时很有用的element. 简单来说,下面用gst-launch这个工具给出一个 ...

  5. 【GStreamer开发】GStreamer基础教程10——GStreamer工具

    目标 GStreamer提供了一系列方便使用的工具.这篇教程里不牵涉任何代码,但还是会讲一些有用的内容: 如何在命令行下建立一个pipeline--完全不使用C 如何找出一个element的Capab ...

  6. GStreamer基础教程02 - 基本概念

    摘要 在 Gstreamer基础教程01 - Hello World中,我们介绍了如何快速的通过一个字符串创建一个简单的pipeline.为了能够更好的控制pipline中的element,我们需要单 ...

  7. GStreamer基础教程09 - Appsrc及Appsink

    摘要 在我们前面的文章中,我们的Pipline都是使用GStreamer自带的插件去产生/消费数据.在实际的情况中,我们的数据源可能没有相应的gstreamer插件,但我们又需要将数据发送到GStre ...

  8. 【GStreamer开发】GStreamer基础教程13——播放速度

    目标 快进,倒放和慢放是trick模式的共同技巧,它们有一个共同点就是它们都修改了播放的速度.本教程会展示如何来获得这些效果和如何进行逐帧的跳跃.主要内容是: 如何来变换播放的速度,变快或者变慢,前进 ...

  9. 【GStreamer开发】GStreamer基础教程08——pipeline的快捷访问

    目标 GStreamer建立的pipeline不需要完全关闭.有多种方法可以让数据在任何时候送到pipeline中或者从pipeline中取出.本教程会展示: 如何把外部数据送到pipeline中 如 ...

随机推荐

  1. Spring Cloud Gateway 之请求坑位[微服务IP不同请求会失败]

    问题产生背景 在使用Spring Cloud Gateway过程中,希望配置多Routes映射不同的微服务,因为Gateway 和Zuul的访问路径不同(zuul 会带有服务service Id),造 ...

  2. python2.x和python3.x版本共存时选择启动的版本

    在windows环境下装好python2.7和python3.6之后,我之前一直是用了很笨的办法去区分版本 那就是把各个版本python安装目录下的python.exe分别改为python2.exe和 ...

  3. Flutter免费(视频)教程汇总

    Flutter学习导航 Flutter简介: Flutter可以轻松快速地构建漂亮的移动应用程序. Flutter是谷歌的移动应用SDK,用于短时间内在iOS和Android上制作高质量的原生界面应用 ...

  4. 决策树(基于增益率)之python实现

    如图,为使用到的公式,信息熵表明样本的混乱程度,增益表示熵减少了,即样本开始分类,增益率是为了平衡增益准则对可取值较多的属性的偏好,同时增益率带来了对可取值偏小的属性的偏好,实际中,先用增益进行筛选, ...

  5. 读《深入理解Elasticsearch》点滴-改善查询相关性

    1.标准查询 query match _all query:"搜索字符串" operator:or 2.多匹配查询+区分权重 query multi_match "que ...

  6. 《老师说的都对》第一次作业:OUC网上课程评价系统

    项目介绍: 项目名称:OUC网上课程评价系统 项目概述:实现一个公开的网上课程评价系统.开课老师也可以在自己的课程主页填写自己的课程内容介绍,学生可以在课程页面中评价自己上过的课程.为想要选课的学生提 ...

  7. Java读源码之Object

    前言 JDK版本: 1.8 最近想看看jdk源码提高下技术深度(比较闲),万物皆对象,虽然Object大多native方法但还是很重要的. 源码 package java.lang; /** * Ja ...

  8. Java 语言特点

    引入<Java核心技术:Ⅰ> 1. 简单性 Java 语法是 C++ 语法的一个“ 纯净” 版本.这里没有头文件. 指针运算(甚至指 针语法).结构. 联合.操作符重载. 虚基类等.如果你 ...

  9. .Net TCP探索(一)——TCP服务端开发(同时监听多个客户端请求)

        最近在园子里看了大神写的(面试官,不要再问我三次握手和四次挥手),忍不住写段程序来测试一番.     在网上找了很多例子,大多只实现了TCP点对点通讯,但实际应用中,一个服务器端口往往要监听多 ...

  10. 章节十七章、2- 给执行失败的case截图

    一.案例演示 1.首先我们把截图的方法单独进行封装方便以后调用. package utilities; import java.io.File; import java.io.IOException; ...