1. 程式人生 > >GStreamer基礎教程11 - 與QT整合

GStreamer基礎教程11 - 與QT整合

摘要

  通常我們的播放引擎需要和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
View Code

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

分別儲存以上內容到各個檔案,執行下列命令即可得到可執行程式。如果找不到標頭檔案及庫檔案,需要根據實際路徑修改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(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的主迴圈及事件處理機制,所以必須要保證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(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內部會自動進行處理。

 

總結

通過本文,我們學習到:

  • 如何使用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 出處:http://www.cnblogs.com/xleng/ 本文版權歸作者所有,歡迎轉載。商業轉載請聯絡作者獲得授權,非商業轉載請在文章頁面明顯位置給出原文連線.