first commit
This commit is contained in:
323
videoviewerwidget.cpp
Normal file
323
videoviewerwidget.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "videoviewerwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <gst/video/videooverlay.h>
|
||||
|
||||
VideoViewerWidget::VideoViewerWidget(QWidget *parent)
|
||||
: QWidget(parent), m_pipeline(nullptr), m_videoSink(nullptr),
|
||||
m_busWatchId(0), m_windowId(0)
|
||||
{
|
||||
initGStreamer();
|
||||
setupUI();
|
||||
}
|
||||
|
||||
VideoViewerWidget::~VideoViewerWidget()
|
||||
{
|
||||
cleanupGStreamer();
|
||||
}
|
||||
|
||||
void VideoViewerWidget::initGStreamer()
|
||||
{
|
||||
gst_init(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void VideoViewerWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// Video display container
|
||||
QGroupBox* videoGroup = new QGroupBox("Video Display", this);
|
||||
QVBoxLayout* videoLayout = new QVBoxLayout();
|
||||
|
||||
m_videoContainer = new QWidget(this);
|
||||
m_videoContainer->setMinimumSize(640, 480);
|
||||
m_videoContainer->setStyleSheet("background-color: black;");
|
||||
m_videoContainer->setAttribute(Qt::WA_NativeWindow);
|
||||
|
||||
videoLayout->addWidget(m_videoContainer);
|
||||
videoGroup->setLayout(videoLayout);
|
||||
|
||||
// Controls
|
||||
QGroupBox* controlGroup = new QGroupBox("Viewer Controls", this);
|
||||
QVBoxLayout* controlLayout = new QVBoxLayout();
|
||||
|
||||
// Source type selection
|
||||
QHBoxLayout* sourceLayout = new QHBoxLayout();
|
||||
sourceLayout->addWidget(new QLabel("Source Type:", this));
|
||||
m_sourceType = new QComboBox(this);
|
||||
m_sourceType->addItem("UDP MJPEG Stream (No plugins needed)", "udp-mjpeg");
|
||||
m_sourceType->addItem("UDP H.264 Stream (Requires gst-libav)", "udp-h264");
|
||||
m_sourceType->addItem("TCP H.264 Stream", "tcp");
|
||||
m_sourceType->addItem("MJPEG HTTP Stream", "http");
|
||||
m_sourceType->addItem("Test Pattern", "test");
|
||||
connect(m_sourceType, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &VideoViewerWidget::onSourceTypeChanged);
|
||||
sourceLayout->addWidget(m_sourceType);
|
||||
|
||||
// Host and port
|
||||
QFormLayout* formLayout = new QFormLayout();
|
||||
m_hostEdit = new QLineEdit("127.0.0.1", this);
|
||||
m_portEdit = new QLineEdit("5000", this);
|
||||
formLayout->addRow("Host:", m_hostEdit);
|
||||
formLayout->addRow("Port:", m_portEdit);
|
||||
|
||||
// Control buttons
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout();
|
||||
m_startBtn = new QPushButton("Start Viewer", this);
|
||||
m_stopBtn = new QPushButton("Stop Viewer", this);
|
||||
m_stopBtn->setEnabled(false);
|
||||
|
||||
connect(m_startBtn, &QPushButton::clicked, this, &VideoViewerWidget::onStartViewer);
|
||||
connect(m_stopBtn, &QPushButton::clicked, this, &VideoViewerWidget::onStopViewer);
|
||||
|
||||
buttonLayout->addWidget(m_startBtn);
|
||||
buttonLayout->addWidget(m_stopBtn);
|
||||
|
||||
// Status label
|
||||
m_statusLabel = new QLabel("Status: Stopped", this);
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }");
|
||||
|
||||
controlLayout->addLayout(sourceLayout);
|
||||
controlLayout->addLayout(formLayout);
|
||||
controlLayout->addLayout(buttonLayout);
|
||||
controlLayout->addWidget(m_statusLabel);
|
||||
controlGroup->setLayout(controlLayout);
|
||||
|
||||
mainLayout->addWidget(videoGroup, 1);
|
||||
mainLayout->addWidget(controlGroup);
|
||||
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
void VideoViewerWidget::showEvent(QShowEvent* event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!m_windowId) {
|
||||
m_videoContainer->winId(); // Force window creation
|
||||
QTimer::singleShot(100, this, [this]() {
|
||||
m_windowId = m_videoContainer->winId();
|
||||
qDebug() << "[VideoViewer] Window ID initialized:" << m_windowId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QString VideoViewerWidget::buildPipelineString()
|
||||
{
|
||||
QString sourceType = m_sourceType->currentData().toString();
|
||||
QString host = m_hostEdit->text();
|
||||
QString port = m_portEdit->text();
|
||||
QString pipeline;
|
||||
|
||||
// Note: Using autovideosink which opens a separate window
|
||||
// VideoOverlay with Qt widgets doesn't work reliably on this system
|
||||
if (sourceType == "udp-mjpeg") {
|
||||
pipeline = QString("udpsrc port=%1 ! application/x-rtp,encoding-name=JPEG,payload=26 ! "
|
||||
"rtpjpegdepay ! jpegdec ! autovideosink")
|
||||
.arg(port);
|
||||
} else if (sourceType == "udp-h264") {
|
||||
pipeline = QString("udpsrc port=%1 ! application/x-rtp,encoding-name=H264 ! "
|
||||
"rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink")
|
||||
.arg(port);
|
||||
} else if (sourceType == "tcp") {
|
||||
pipeline = QString("tcpclientsrc host=%1 port=%2 ! tsdemux ! h264parse ! avdec_h264 ! "
|
||||
"videoconvert ! autovideosink")
|
||||
.arg(host).arg(port);
|
||||
} else if (sourceType == "http") {
|
||||
pipeline = QString("souphttpsrc location=http://%1:%2 ! multipartdemux ! jpegdec ! "
|
||||
"videoconvert ! autovideosink")
|
||||
.arg(host).arg(port);
|
||||
} else if (sourceType == "test") {
|
||||
pipeline = "videotestsrc ! autovideosink";
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void VideoViewerWidget::startPipeline()
|
||||
{
|
||||
if (m_pipeline) {
|
||||
stopPipeline();
|
||||
}
|
||||
|
||||
QString pipelineStr = buildPipelineString();
|
||||
qDebug() << "[VideoViewer] Starting pipeline:" << pipelineStr;
|
||||
|
||||
GError* error = nullptr;
|
||||
|
||||
m_pipeline = gst_parse_launch(pipelineStr.toUtf8().constData(), &error);
|
||||
|
||||
if (error) {
|
||||
m_statusLabel->setText(QString("Status: Pipeline Error - %1").arg(error->message));
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }");
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_pipeline) {
|
||||
m_statusLabel->setText("Status: Failed to create pipeline");
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up bus callback
|
||||
GstBus* bus = gst_element_get_bus(m_pipeline);
|
||||
m_busWatchId = gst_bus_add_watch(bus, busCallback, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
// Note: VideoOverlay disabled - using autovideosink with separate window instead
|
||||
|
||||
// Start playing
|
||||
GstStateChangeReturn ret = gst_element_set_state(m_pipeline, GST_STATE_PLAYING);
|
||||
|
||||
qDebug() << "[VideoViewer] Pipeline state change return:" << ret;
|
||||
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
m_statusLabel->setText("Status: Failed to start pipeline");
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }");
|
||||
cleanupGStreamer();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "[VideoViewer] Pipeline started successfully";
|
||||
m_statusLabel->setText("Status: Playing");
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #90EE90; padding: 5px; border-radius: 3px; }");
|
||||
m_startBtn->setEnabled(false);
|
||||
m_stopBtn->setEnabled(true);
|
||||
}
|
||||
|
||||
void VideoViewerWidget::stopPipeline()
|
||||
{
|
||||
if (m_pipeline) {
|
||||
gst_element_set_state(m_pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(m_pipeline);
|
||||
m_pipeline = nullptr;
|
||||
}
|
||||
|
||||
if (m_videoSink) {
|
||||
gst_object_unref(m_videoSink);
|
||||
m_videoSink = nullptr;
|
||||
}
|
||||
|
||||
if (m_busWatchId > 0) {
|
||||
g_source_remove(m_busWatchId);
|
||||
m_busWatchId = 0;
|
||||
}
|
||||
|
||||
m_statusLabel->setText("Status: Stopped");
|
||||
m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }");
|
||||
m_startBtn->setEnabled(true);
|
||||
m_stopBtn->setEnabled(false);
|
||||
}
|
||||
|
||||
void VideoViewerWidget::cleanupGStreamer()
|
||||
{
|
||||
stopPipeline();
|
||||
}
|
||||
|
||||
gboolean VideoViewerWidget::busCallback(GstBus* bus, GstMessage* msg, gpointer data)
|
||||
{
|
||||
VideoViewerWidget* viewer = static_cast<VideoViewerWidget*>(data);
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_ERROR: {
|
||||
GError* err;
|
||||
gchar* debug_info;
|
||||
gst_message_parse_error(msg, &err, &debug_info);
|
||||
|
||||
QString errorMsg = QString("GStreamer Error: %1\nDebug: %2")
|
||||
.arg(err->message)
|
||||
.arg(debug_info ? debug_info : "none");
|
||||
|
||||
qDebug() << "[VideoViewer] ERROR:" << errorMsg;
|
||||
|
||||
QMetaObject::invokeMethod(viewer, [viewer, errorMsg]() {
|
||||
viewer->m_statusLabel->setText("Status: Stream Error - " + errorMsg);
|
||||
viewer->m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }");
|
||||
viewer->stopPipeline();
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
g_error_free(err);
|
||||
g_free(debug_info);
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_EOS:
|
||||
qDebug() << "[VideoViewer] End of stream";
|
||||
QMetaObject::invokeMethod(viewer, [viewer]() {
|
||||
viewer->m_statusLabel->setText("Status: End of Stream");
|
||||
viewer->stopPipeline();
|
||||
}, Qt::QueuedConnection);
|
||||
break;
|
||||
case GST_MESSAGE_STATE_CHANGED:
|
||||
if (GST_MESSAGE_SRC(msg) == GST_OBJECT(viewer->m_pipeline)) {
|
||||
GstState oldState, newState, pendingState;
|
||||
gst_message_parse_state_changed(msg, &oldState, &newState, &pendingState);
|
||||
qDebug() << "[VideoViewer] State changed:"
|
||||
<< gst_element_state_get_name(oldState) << "->"
|
||||
<< gst_element_state_get_name(newState);
|
||||
}
|
||||
break;
|
||||
case GST_MESSAGE_WARNING: {
|
||||
GError* err;
|
||||
gchar* debug_info;
|
||||
gst_message_parse_warning(msg, &err, &debug_info);
|
||||
qDebug() << "[VideoViewer] WARNING:" << err->message;
|
||||
g_error_free(err);
|
||||
g_free(debug_info);
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_INFO: {
|
||||
GError* err;
|
||||
gchar* debug_info;
|
||||
gst_message_parse_info(msg, &err, &debug_info);
|
||||
qDebug() << "[VideoViewer] INFO:" << err->message;
|
||||
g_error_free(err);
|
||||
g_free(debug_info);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void VideoViewerWidget::onStartViewer()
|
||||
{
|
||||
startPipeline();
|
||||
}
|
||||
|
||||
void VideoViewerWidget::onStopViewer()
|
||||
{
|
||||
stopPipeline();
|
||||
}
|
||||
|
||||
void VideoViewerWidget::onSourceTypeChanged(int index)
|
||||
{
|
||||
QString sourceType = m_sourceType->currentData().toString();
|
||||
|
||||
bool needsNetwork = (sourceType != "test");
|
||||
bool isUdp = (sourceType == "udp-mjpeg" || sourceType == "udp-h264");
|
||||
m_hostEdit->setEnabled(needsNetwork && !isUdp);
|
||||
m_portEdit->setEnabled(needsNetwork);
|
||||
}
|
||||
|
||||
void VideoViewerWidget::onPrepareWindowHandle(GstBus* bus, GstMessage* msg, gpointer data)
|
||||
{
|
||||
if (!gst_is_video_overlay_prepare_window_handle_message(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
VideoViewerWidget* viewer = static_cast<VideoViewerWidget*>(data);
|
||||
|
||||
if (viewer->m_windowId) {
|
||||
GstElement* sink = GST_ELEMENT(GST_MESSAGE_SRC(msg));
|
||||
qDebug() << "[VideoViewer] prepare-window-handle: Setting window ID" << viewer->m_windowId;
|
||||
gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(sink), viewer->m_windowId);
|
||||
} else {
|
||||
qDebug() << "[VideoViewer] prepare-window-handle: No window ID available yet";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user