728x90

이전 튜토리얼까지 진행하면서 gstreamer에 대한 기초를 살펴봤습니다. 이를 활용해서 간단한 미디어 플레이어를 제작해보는 예제를 진행하려고 합니다. Tutorial 5는 GTK를 기반으로 예제를 진행하기 때문에 많은 참고는 할 수 없지만 여기서 중요한 함수를 확인할 수 있습니다.

 

Basic tutorial 5: GUI toolkit integration

Please port this tutorial to python! Please port this tutorial to javascript! Goal This tutorial shows how to integrate GStreamer in a Graphical User Interface (GUI) toolkit like GTK+. Basically, GStreamer takes care of media playback while the GUI toolkit

gstreamer.freedesktop.org

이전까지 예제를 실행하면 Direct 3D 11 renders라는 창이 뜨면서 영상이 재생되었습니다. 이 창을 저희 UI에 이식시켜야 하는게 가장 문제였는데 아래 함수를 통해 이 문제를 해결할 수 있습니다.

 

gst_video_overlay_set_window_handle을 통해 위젯의 handle 값을 넘겨 해당 위젯에서 Play(Draw)가 가능합니다.

그리고 해당 함수를 사용하기 위해서는 lgstvideo-1.0 라이브러리를 사용해야 하기 때문에 project 파일도 아래처럼 추가하였습니다.

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    MsgBusCheckThread.cpp \
    StreamMgr.cpp \
    main.cpp \
    GstreamerPlayer.cpp

HEADERS += \
    Common.h \
    GstreamerPlayer.h \
    MsgBusCheckThread.h \
    StreamMgr.h

FORMS += \
    GstreamerPlayer.ui

INCLUDEPATH += \
    $$PWD/include \
    $$PWD/include/gstreamer-1.0 \
    $$PWD/include/glib-2.0/ \
    $$PWD/include/glib-2.0/include \
    $$PWD/lib/glib-2.0/include \
    $$PWD/include/orc-0.4 \


win32: LIBS += -L$$PWD/lib/ -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0

DESTDIR += \
    $$PWD/bin

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

실행화면은 아래와 같습니다. 실제 play 영상은 QFrame 안에 재생이 되고 있고 Play, Pause, Stop 버튼과 재생 길이를 확인할 수 있는 Slider와 그 밑에는 현재 영상 시점, 영상 길이를 알 수 있는 Label로 이루어져 있습니다.

GstreamerPlayer MainWindow 코드입니다. StreamMgr를 통해 Gstreamer를 control 합니다. play 시에 QFrame의 Handle 값을 넘겨주게 됩니다. StreamMgr를 통해 받아온 영상 시간 관련 정보를 label과 slider에 표시해줍니다.

#include "GstreamerPlayer.h"
#include "ui_GstreamerPlayer.h"
#include <QTime>

GstreamerPlayer::GstreamerPlayer(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::GstreamerPlayer)
    , pMgr(NULL)
{
    ui->setupUi(this);

    pMgr = new StreamMgr(this);

    connect(ui->btnPlay,    SIGNAL(clicked(bool)),            this, SLOT(Slot_Play()));
    connect(ui->btnPause,   SIGNAL(clicked(bool)),            this, SLOT(Slot_Pause()));
    connect(ui->btnStop,    SIGNAL(clicked(bool)),            this, SLOT(Slot_Stop()));
    connect(pMgr,           SIGNAL(emit_changeDuration(int)), this, SLOT(Slot_StreamDuration(int)));
    connect(pMgr,           SIGNAL(emit_currentTime(int)),    this, SLOT(Slot_CurrentTime(int)));
}

GstreamerPlayer::~GstreamerPlayer()
{
    delete ui;
    delete pMgr;
}

void GstreamerPlayer::Slot_Play()
{
    if ( pMgr == NULL ) { return; }
    pMgr->Play();
    pMgr->SetWindowHandle(ui->frame->winId());
    // QFrame의 Handle 값 전달
}

void GstreamerPlayer::Slot_Pause()
{
    if ( pMgr == NULL ) { return; }
    pMgr->Pause();
}

void GstreamerPlayer::Slot_Stop()
{
    if ( pMgr == NULL ) { return; }
    pMgr->Stop();
}

void GstreamerPlayer::Slot_StreamDuration(int nDuration)
{
    ui->bar->setRange(0, nDuration);
    QTime time(0,0,0);
    time = time.addSecs(nDuration);
    ui->lbDuration->setText(time.toString());
}

void GstreamerPlayer::Slot_CurrentTime(int nTime)
{
    ui->bar->setValue(nTime);
    QTime time(0,0,0);
    time = time.addSecs(nTime);
    ui->lbCurrent->setText(time.toString());
}

전체적인 Gstreamer 관리는 StremMgr이란 클래스에서 이뤄집니다. 이 클래스에서 체크해야할 점은 msg를 처리하기 위해 Thread를 따로 생성했다는 것과 GstreamerPlayer에서 전달받은 Handle 값을 gst_video_overlay_set_window_handle에 넘겨줍니다.

#include <QDebug>
#include "StreamMgr.h"
#include "gst/video/videooverlay.h"

StreamMgr::StreamMgr(QObject *parent)
    : QObject{parent}
    , pThread(NULL)
{
    Init();

    pThread = new MsgBusCheckThread();

    if ( pThread == NULL || bus == NULL || data == NULL ) { return; }

    connect(pThread, SIGNAL(emit_changeDuration(int)), this, SLOT(slot_getDuration(int)));
    connect(pThread, SIGNAL(emit_currentTime(int)),    this, SLOT(slot_getTime(int)));

    pThread->SetBus(bus);
    pThread->setData(data);
    pThread->start();
}

StreamMgr::~StreamMgr()
{
    if ( pThread->isRunning() ) {
        pThread->quit();
        pThread->wait();
    }

    delete data;
}

void StreamMgr::Init()
{
    /* Initialize GStreamer */
    gst_init (NULL, NULL);

    data = new CustomData;
    if ( data == NULL ) { return; }

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

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

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

    /* Listen to the bus */
    bus = gst_element_get_bus (data->playbin);
}

void StreamMgr::slot_getDuration(int nDuration)
{
    emit emit_changeDuration(nDuration);
}

void StreamMgr::slot_getTime(int nTime)
{
    emit emit_currentTime(nTime);
}

void StreamMgr::SetPlayBinState(GstState State)
{
    ret = gst_element_set_state (data->playbin, State);
    if (ret == GST_STATE_CHANGE_FAILURE) {
        g_printerr ("Unable to set the pipeline to the playing state.\n");
        gst_object_unref (data->playbin);
        return;
    }
}

void StreamMgr::Pause()
{
    SetPlayBinState(GST_STATE_PAUSED);
}

void StreamMgr::Play()
{
    SetPlayBinState(GST_STATE_PLAYING);
}

void StreamMgr::Stop()
{
    SetPlayBinState(GST_STATE_READY);
}

void StreamMgr::SetWindowHandle(qint64 nID)
{
    gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY (data->playbin), static_cast<guintptr>(nID));
}

마지막으로 msg를 처리할 Thread입니다. 나머지 코드는 이전 튜토리얼과 크게 다를것이 없고 gstTimeToSecond를 통해 GST_TIME_FORMAT 형태의 시간 표시를 QTime을 통해 Seconds 값으로 변경해줍니다.

#include "MsgBusCheckThread.h"
#include <QDebug>
#include <QTime>

MsgBusCheckThread::MsgBusCheckThread()
    : QThread()
    , bus(NULL)
    , msg(NULL)
    , data(NULL)
{
}

void MsgBusCheckThread::run()
{
    if ( bus == NULL ) { return; }

    while (true) {
        msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND, (GstMessageType)(GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION));

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

            /* Query the current position of the stream */
            if (gst_element_query_position (data->playbin, GST_FORMAT_TIME, &current)) {
                int nCurrent = gstTimeToSecond(current);
                emit emit_currentTime(nCurrent);
            }

            /* 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)) {
                    int nDuration = gstTimeToSecond(data->duration);
                    emit emit_changeDuration(nDuration);
                }
            }
        }
    }
}

void MsgBusCheckThread::HandleMessage(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 ("\nEnd-Of-Stream reached.\n");
        data->terminate = TRUE;
        break;
    case GST_MESSAGE_DURATION:
        /* 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);
}

int MsgBusCheckThread::gstTimeToSecond(gint64 nTime)
{
    QString sTime = QString::asprintf("%" GST_TIME_FORMAT, GST_TIME_ARGS (nTime));
    sTime = sTime.remove(sTime.indexOf("."),sTime.length()-sTime.indexOf("."));
    QTime tTime = QTime::fromString(sTime,"h:mm:ss");
    int nSecond = QTime(0,0,0).secsTo(tTime);
    return nSecond;
}

void MsgBusCheckThread::SetBus(GstBus *pBus)
{
    bus = pBus;
}

void MsgBusCheckThread::setData(CustomData *pData)
{
    data = pData;
}

자세한 코드는 아래 링크 참고 부탁드립니다. Seek 및 기타 기능은 다음 기회에 추가해보도록 하겠습니다.

 

GitHub - psy1064/Gstreamer-Qt: Gstreamer example using Qt5

Gstreamer example using Qt5. Contribute to psy1064/Gstreamer-Qt development by creating an account on GitHub.

github.com

 

728x90

+ Recent posts