Files
gstreamerViewer/gstreamerpipelinewidget.cpp
2025-12-19 09:50:35 +01:00

328 lines
13 KiB
C++

/*
* GStreamer Camera Viewer
* Copyright (c) 2025 Maik Jurischka
* Licensed under CC BY-NC-SA 4.0
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
#include "gstreamerpipelinewidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QFrame>
#include <QTimer>
#include <QJsonArray>
GStreamerPipelineWidget::GStreamerPipelineWidget(SocketClient* socketClient, QWidget *parent)
: QWidget(parent), m_socketClient(socketClient)
{
setupUI();
onGetStatus();
fetchAvailableFormats();
}
void GStreamerPipelineWidget::setupUI()
{
auto* mainLayout = new QVBoxLayout(this);
auto* groupBox = new QGroupBox("GStreamer Pipeline", this);
auto* groupLayout = new QVBoxLayout(groupBox);
m_infoLabel = new QLabel(
"<b>Quick Start:</b> Click 'Quick Start' to automatically configure and start streaming.<br>"
"<b>Manual:</b> 1. Set video format → 2. Set pipeline → 3. Start stream", this);
m_infoLabel->setStyleSheet("QLabel { background-color: #e3f2fd; padding: 8px; border-radius: 4px; }");
m_infoLabel->setWordWrap(true);
groupLayout->addWidget(m_infoLabel);
m_quickStartBtn = new QPushButton("⚡ Quick Start (Auto Configure & Stream)", this);
m_quickStartBtn->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 10px; }");
connect(m_quickStartBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onQuickStart);
groupLayout->addWidget(m_quickStartBtn);
auto* line = new QFrame(this);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
groupLayout->addWidget(line);
auto* formatLabel = new QLabel("Video Format:", this);
m_formatCombo = new QComboBox(this);
m_formatCombo->addItem("1280x720@30fps UYVY (Default/Supported)", "1280,720,30,UYVY");
m_formatCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
m_formatCombo->setMinimumContentsLength(20);
groupLayout->addWidget(formatLabel);
groupLayout->addWidget(m_formatCombo);
auto* presetsLabel = new QLabel("Pipeline Presets:", this);
m_pipelinePresets = new QComboBox(this);
m_pipelinePresets->addItem("MJPEG UDP Stream (Best for raw formats)", "videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000");
m_pipelinePresets->addItem("UDP H.264 Stream (Requires gst-libav)", "videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=127.0.0.1 port=5000");
m_pipelinePresets->addItem("Custom", "");
m_pipelinePresets->addItem("Test - Fake Sink (No Output)", "fakesink");
m_pipelinePresets->addItem("Local Display", "videoconvert ! autovideosink");
m_pipelinePresets->addItem("TCP H.264 Stream", "videoconvert ! x264enc ! h264parse ! mpegtsmux ! tcpserversink host=0.0.0.0 port=5000");
m_pipelinePresets->addItem("MJPEG HTTP Stream", "videoconvert ! jpegenc ! multipartmux ! tcpserversink host=0.0.0.0 port=8080");
m_pipelinePresets->addItem("Save to File", "videoconvert ! x264enc ! mp4mux ! filesink location=/tmp/output.mp4");
m_pipelinePresets->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
m_pipelinePresets->setMinimumContentsLength(25);
connect(m_pipelinePresets, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &GStreamerPipelineWidget::onPipelinePresetChanged);
groupLayout->addWidget(presetsLabel);
groupLayout->addWidget(m_pipelinePresets);
auto* pipelineLabel = new QLabel("Pipeline:", this);
m_pipelineEdit = new QTextEdit(this);
m_pipelineEdit->setMaximumHeight(80);
m_pipelineEdit->setPlaceholderText("Enter GStreamer pipeline here...\nExample: videoconvert ! autovideosink");
groupLayout->addWidget(pipelineLabel);
groupLayout->addWidget(m_pipelineEdit);
m_setPipelineBtn = new QPushButton("Set Pipeline", this);
connect(m_setPipelineBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onSetPipeline);
groupLayout->addWidget(m_setPipelineBtn);
auto* buttonLayout = new QHBoxLayout();
m_startStreamBtn = new QPushButton("Start Stream", this);
m_stopStreamBtn = new QPushButton("Stop Stream", this);
m_getStatusBtn = new QPushButton("Get Status", this);
connect(m_startStreamBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onStartStream);
connect(m_stopStreamBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onStopStream);
connect(m_getStatusBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onGetStatus);
buttonLayout->addWidget(m_startStreamBtn);
buttonLayout->addWidget(m_stopStreamBtn);
buttonLayout->addWidget(m_getStatusBtn);
groupLayout->addLayout(buttonLayout);
m_statusLabel = new QLabel("Status: Unknown", this);
m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }");
groupLayout->addWidget(m_statusLabel);
groupBox->setMaximumWidth(500);
mainLayout->addWidget(groupBox);
mainLayout->addStretch();
setLayout(mainLayout);
}
void GStreamerPipelineWidget::onPipelinePresetChanged(int index)
{
QString pipeline = m_pipelinePresets->currentData().toString();
if (!pipeline.isEmpty()) {
m_pipelineEdit->setPlainText(pipeline);
}
}
void GStreamerPipelineWidget::onSetPipeline()
{
QString pipeline = m_pipelineEdit->toPlainText().trimmed();
if (pipeline.isEmpty()) {
updateStatus("Error: Empty pipeline", false);
return;
}
QJsonObject params;
params["pipeline"] = pipeline;
m_socketClient->sendCommand("set_pipeline", params,
[this](const QJsonObject& response) {
updateStatus("Pipeline set successfully", false);
},
[this](const QString& error) {
updateStatus("Error: " + error, false);
});
}
void GStreamerPipelineWidget::onStartStream()
{
// First ensure format is set, then start stream
QString formatData = m_formatCombo->currentData().toString();
QStringList parts = formatData.split(',');
QJsonObject formatParams;
formatParams["width"] = parts[0];
formatParams["height"] = parts[1];
formatParams["framerate"] = parts[2];
formatParams["format"] = parts[3];
m_socketClient->sendCommand("set_format", formatParams,
[this](const QJsonObject& response) {
m_socketClient->sendCommand("start_stream", QJsonObject(),
[this](const QJsonObject& response) {
updateStatus("Streaming started", true);
},
[this](const QString& error) {
updateStatus("Error: Failed to start stream: " + error, false);
});
},
[this](const QString& error) {
// Format setting failed, but maybe it was already set - try starting anyway
m_socketClient->sendCommand("start_stream", QJsonObject(),
[this](const QJsonObject& response) {
updateStatus("Streaming started", true);
},
[this](const QString& error) {
updateStatus("Error: Failed to start stream: " + error, false);
});
});
}
void GStreamerPipelineWidget::onStopStream()
{
m_socketClient->sendCommand("stop_stream", QJsonObject(),
[this](const QJsonObject& response) {
updateStatus("Streaming stopped", false);
},
[this](const QString& error) {
updateStatus("Error: Failed to stop stream: " + error, false);
});
}
void GStreamerPipelineWidget::onGetStatus()
{
m_socketClient->sendCommand("get_status", QJsonObject(),
[this](const QJsonObject& response) {
bool streaming = response["streaming"].toBool();
QString pipeline = response["pipeline"].toString();
updateStatus(streaming ? "Streaming" : "Stopped", streaming);
if (!pipeline.isEmpty() && m_pipelineEdit->toPlainText().isEmpty()) {
m_pipelineEdit->setPlainText(pipeline);
}
},
[this](const QString& error) {
updateStatus("Connection Error", false);
});
}
void GStreamerPipelineWidget::updateStatus(const QString& status, bool streaming)
{
m_statusLabel->setText("Status: " + status);
if (streaming) {
m_statusLabel->setStyleSheet("QLabel { background-color: #90EE90; padding: 5px; border-radius: 3px; }");
} else if (status.contains("Error")) {
m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }");
} else {
m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }");
}
}
void GStreamerPipelineWidget::onQuickStart()
{
m_quickStartBtn->setEnabled(false);
m_quickStartBtn->setText("Configuring...");
// Step 1: Set format
QString formatData = m_formatCombo->currentData().toString();
QStringList parts = formatData.split(',');
QJsonObject formatParams;
formatParams["width"] = parts[0];
formatParams["height"] = parts[1];
formatParams["framerate"] = parts[2];
formatParams["format"] = parts[3];
m_socketClient->sendCommand("set_format", formatParams,
[this](const QJsonObject& response) {
// Step 2: Use selected preset pipeline or default to MJPEG
QString pipeline = m_pipelinePresets->currentData().toString();
if (pipeline.isEmpty()) {
pipeline = "videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000";
}
m_pipelineEdit->setPlainText(pipeline);
QJsonObject pipelineParams;
pipelineParams["pipeline"] = pipeline;
m_socketClient->sendCommand("set_pipeline", pipelineParams,
[this](const QJsonObject& response) {
// Step 3: Start stream
m_socketClient->sendCommand("start_stream", QJsonObject(),
[this](const QJsonObject& response) {
updateStatus("Streaming started - Click 'Start Viewer' on the left to view", true);
m_quickStartBtn->setEnabled(true);
m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)");
},
[this](const QString& error) {
m_quickStartBtn->setEnabled(true);
m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)");
updateStatus("Error: Failed to start stream: " + error, false);
});
},
[this](const QString& error) {
m_quickStartBtn->setEnabled(true);
m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)");
updateStatus("Error: Failed to set pipeline: " + error, false);
});
},
[this](const QString& error) {
m_quickStartBtn->setEnabled(true);
m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)");
updateStatus("Error: Failed to set format: " + error, false);
});
}
void GStreamerPipelineWidget::setFormatAndPipeline()
{
// Called before manual stream start to ensure format is set
QString formatData = m_formatCombo->currentData().toString();
QStringList parts = formatData.split(',');
QJsonObject formatParams;
formatParams["width"] = parts[0];
formatParams["height"] = parts[1];
formatParams["framerate"] = parts[2];
formatParams["format"] = parts[3];
m_socketClient->sendCommand("set_format", formatParams,
[](const QJsonObject& response) {},
[](const QString& error) {});
}
void GStreamerPipelineWidget::fetchAvailableFormats()
{
m_socketClient->sendCommand("get_formats", QJsonObject(),
[this](const QJsonObject& response) {
onFormatsReceived(response);
},
[this](const QString& error) {
qDebug() << "Failed to fetch formats:" << error;
});
}
void GStreamerPipelineWidget::onFormatsReceived(const QJsonObject& response)
{
if (!response.contains("formats")) {
return;
}
QJsonArray formats = response["formats"].toArray();
if (formats.isEmpty()) {
return;
}
m_formatCombo->clear();
for (const QJsonValue& val : formats) {
QJsonObject fmt = val.toObject();
int width = fmt["width"].toInt();
int height = fmt["height"].toInt();
int fps = fmt["framerate"].toInt();
QString format = fmt["format"].toString();
QString displayText = QString("%1x%2@%3fps %4").arg(width, height, fps).arg(format);
QString data = QString("%1,%2,%3,%4").arg(width).arg(height).arg(fps).arg(format);
m_formatCombo->addItem(displayText, data);
}
qDebug() << "Loaded" << formats.size() << "available formats from camera";
}