- Grafische Ausgabe kann nun als Video aufgezeichnet werden

- Verbindung über TCP/IP
This commit is contained in:
2026-08-18 07:34:20 +02:00
parent a942f8385b
commit 36923e1646
16 changed files with 724 additions and 311 deletions

View File

@@ -10,24 +10,28 @@
ConnectDialog::ConnectDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Connect to UART"));
setMinimumWidth(360);
setWindowTitle(tr("Connect to UARTScope input"));
setMinimumWidth(380);
auto *form = new QFormLayout();
m_modeTabs = new QTabWidget(this);
// ── "Seriell" tab: real or virtual (PTY) serial port ────────────────
auto *serialTab = new QWidget(m_modeTabs);
auto *form = new QFormLayout(serialTab);
form->setRowWrapPolicy(QFormLayout::DontWrapRows);
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
auto *portRow = new QHBoxLayout();
m_portCombo = new QComboBox(this);
m_portCombo = new QComboBox(serialTab);
m_portCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto *refreshBtn = new QPushButton(tr(""), this);
auto *refreshBtn = new QPushButton(tr(""), serialTab);
refreshBtn->setMaximumWidth(30);
connect(refreshBtn, &QPushButton::clicked, this, &ConnectDialog::refreshPorts);
portRow->addWidget(m_portCombo, 1);
portRow->addWidget(refreshBtn);
form->addRow(tr("Port:"), portRow);
m_baudCombo = new QComboBox(this);
m_baudCombo = new QComboBox(serialTab);
const QList<int> bauds = {1200, 2400, 4800, 9600, 19200, 38400,
57600, 115200, 230400, 460800, 921600};
for (int b : bauds)
@@ -35,7 +39,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_baudCombo->setCurrentText("115200");
form->addRow(tr("Baud rate:"), m_baudCombo);
m_dataBitsCombo = new QComboBox(this);
m_dataBitsCombo = new QComboBox(serialTab);
m_dataBitsCombo->addItem("5", QSerialPort::Data5);
m_dataBitsCombo->addItem("6", QSerialPort::Data6);
m_dataBitsCombo->addItem("7", QSerialPort::Data7);
@@ -43,7 +47,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_dataBitsCombo->setCurrentText("8");
form->addRow(tr("Data bits:"), m_dataBitsCombo);
m_parityCombo = new QComboBox(this);
m_parityCombo = new QComboBox(serialTab);
m_parityCombo->addItem(tr("None"), QSerialPort::NoParity);
m_parityCombo->addItem(tr("Even"), QSerialPort::EvenParity);
m_parityCombo->addItem(tr("Odd"), QSerialPort::OddParity);
@@ -51,18 +55,52 @@ ConnectDialog::ConnectDialog(QWidget *parent)
m_parityCombo->addItem(tr("Mark"), QSerialPort::MarkParity);
form->addRow(tr("Parity:"), m_parityCombo);
m_stopBitsCombo = new QComboBox(this);
m_stopBitsCombo = new QComboBox(serialTab);
m_stopBitsCombo->addItem("1", QSerialPort::OneStop);
m_stopBitsCombo->addItem("1.5", QSerialPort::OneAndHalfStop);
m_stopBitsCombo->addItem("2", QSerialPort::TwoStop);
form->addRow(tr("Stop bits:"), m_stopBitsCombo);
m_flowCombo = new QComboBox(this);
m_flowCombo = new QComboBox(serialTab);
m_flowCombo->addItem(tr("None"), QSerialPort::NoFlowControl);
m_flowCombo->addItem(tr("RTS/CTS"), QSerialPort::HardwareControl);
m_flowCombo->addItem(tr("XON/XOFF"), QSerialPort::SoftwareControl);
form->addRow(tr("Flow control:"), m_flowCombo);
m_modeTabs->addTab(serialTab, tr("Seriell"));
// ── "Netzwerk (TCP)" tab: e.g. FS-UAE's serial-over-socat output ────
auto *networkTab = new QWidget(m_modeTabs);
auto *netForm = new QFormLayout(networkTab);
netForm->setRowWrapPolicy(QFormLayout::DontWrapRows);
netForm->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_hostEdit = new QLineEdit(networkTab);
m_hostEdit->setPlaceholderText(tr("z. B. localhost oder 192.168.1.50"));
m_hostEdit->setText("localhost");
netForm->addRow(tr("Host / IP:"), m_hostEdit);
m_portSpin = new QSpinBox(networkTab);
m_portSpin->setRange(1, 65535);
m_portSpin->setValue(1234);
netForm->addRow(tr("Port:"), m_portSpin);
auto *netInfoLabel = new QLabel(
tr("Für Emulatoren/Firmware, die die UART-Ausgabe über TCP senden "
"statt über ein echtes serielles Gerät (z. B. FS-UAE, dessen "
"serielle Schnittstelle per <tt>socat</tt> auf einen TCP-Port "
"gelegt wird): hier Host und Port statt eines seriellen Ports "
"angeben. Baudrate/Parität etc. spielen dabei keine Rolle."),
networkTab);
netInfoLabel->setWordWrap(true);
netInfoLabel->setStyleSheet("color: gray; font-size: 10px;");
netForm->addRow(netInfoLabel);
m_modeTabs->addTab(networkTab, tr("Netzwerk (TCP)"));
// ── shared: log file (applies to either transport) ──────────────────
auto *logForm = new QFormLayout();
logForm->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
auto *logRow = new QHBoxLayout();
m_logPathEdit = new QLineEdit(this);
m_logPathEdit->setPlaceholderText(tr("(optional) path/to/output.log"));
@@ -71,7 +109,7 @@ ConnectDialog::ConnectDialog(QWidget *parent)
connect(browseBtn, &QPushButton::clicked, this, &ConnectDialog::browseLogFile);
logRow->addWidget(m_logPathEdit, 1);
logRow->addWidget(browseBtn);
form->addRow(tr("Log file:"), logRow);
logForm->addRow(tr("Log file:"), logRow);
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
@@ -79,7 +117,8 @@ ConnectDialog::ConnectDialog(QWidget *parent)
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(form);
mainLayout->addWidget(m_modeTabs);
mainLayout->addLayout(logForm);
mainLayout->addWidget(buttons);
refreshPorts();
@@ -146,12 +185,20 @@ void ConnectDialog::browseLogFile()
SerialConfig ConnectDialog::config() const
{
SerialConfig cfg;
cfg.connectionType = (m_modeTabs->currentIndex() == 1)
? SerialConfig::ConnectionType::Network
: SerialConfig::ConnectionType::Serial;
cfg.portName = m_portCombo->currentData().toString();
cfg.baudRate = m_baudCombo->currentData().toInt();
cfg.dataBits = static_cast<QSerialPort::DataBits>(m_dataBitsCombo->currentData().toInt());
cfg.parity = static_cast<QSerialPort::Parity>(m_parityCombo->currentData().toInt());
cfg.stopBits = static_cast<QSerialPort::StopBits>(m_stopBitsCombo->currentData().toInt());
cfg.flowControl = static_cast<QSerialPort::FlowControl>(m_flowCombo->currentData().toInt());
cfg.networkHost = m_hostEdit->text().trimmed();
cfg.networkPort = static_cast<quint16>(m_portSpin->value());
cfg.logFilePath = m_logPathEdit->text().trimmed();
return cfg;
}

View File

@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("UARTScope");
app.setApplicationVersion("1.0.0");
app.setApplicationVersion("1.1.0");
app.setOrganizationName("ChicaDev");
app.setStyle(QStyleFactory::create("Fusion"));

View File

@@ -117,6 +117,7 @@ void MainWindow::setupUi()
void MainWindow::setupToolBar()
{
auto *tb = addToolBar(tr("Main"));
tb->setObjectName("mainToolBar"); // required by QMainWindow::saveState()
tb->setMovable(false);
m_connectAction = tb->addAction(tr("Connect…"));
@@ -199,9 +200,17 @@ void MainWindow::onConnectClicked()
return;
m_lastConfig = dlg.config();
if (m_lastConfig.portName.isEmpty()) {
QMessageBox::warning(this, tr("No port"), tr("Please select a valid serial port."));
return;
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
if (m_lastConfig.portName.isEmpty()) {
QMessageBox::warning(this, tr("No port"), tr("Please select a valid serial port."));
return;
}
} else {
if (m_lastConfig.networkHost.isEmpty()) {
QMessageBox::warning(this, tr("No host"), tr("Please enter a host/IP to connect to."));
return;
}
}
if (!m_lastConfig.logFilePath.isEmpty())
@@ -216,14 +225,21 @@ void MainWindow::onConnectClicked()
m_worker->setReconnectInterval(interval);
}, Qt::QueuedConnection);
QMetaObject::invokeMethod(m_worker, "openPort",
Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.portName),
Q_ARG(qint32, m_lastConfig.baudRate),
Q_ARG(QSerialPort::DataBits, m_lastConfig.dataBits),
Q_ARG(QSerialPort::Parity, m_lastConfig.parity),
Q_ARG(QSerialPort::StopBits, m_lastConfig.stopBits),
Q_ARG(QSerialPort::FlowControl, m_lastConfig.flowControl));
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Serial) {
QMetaObject::invokeMethod(m_worker, "openPort",
Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.portName),
Q_ARG(qint32, m_lastConfig.baudRate),
Q_ARG(QSerialPort::DataBits, m_lastConfig.dataBits),
Q_ARG(QSerialPort::Parity, m_lastConfig.parity),
Q_ARG(QSerialPort::StopBits, m_lastConfig.stopBits),
Q_ARG(QSerialPort::FlowControl, m_lastConfig.flowControl));
} else {
QMetaObject::invokeMethod(m_worker, "openNetwork",
Qt::QueuedConnection,
Q_ARG(QString, m_lastConfig.networkHost),
Q_ARG(quint16, m_lastConfig.networkPort));
}
}
void MainWindow::onDisconnectClicked()
@@ -238,10 +254,17 @@ void MainWindow::onPortOpened()
m_stateLabel->setText(tr("● Connected"));
m_stateLabel->setStyleSheet("color: #4ec94e;");
m_reconnectLabel->clear();
m_portLabel->setText(
QStringLiteral("%1 @ %2 baud")
.arg(m_lastConfig.portName)
.arg(m_lastConfig.baudRate));
if (m_lastConfig.connectionType == SerialConfig::ConnectionType::Network) {
m_portLabel->setText(
QStringLiteral("%1:%2 (TCP)")
.arg(m_lastConfig.networkHost)
.arg(m_lastConfig.networkPort));
} else {
m_portLabel->setText(
QStringLiteral("%1 @ %2 baud")
.arg(m_lastConfig.portName)
.arg(m_lastConfig.baudRate));
}
statusBar()->clearMessage();
}
@@ -299,12 +322,15 @@ void MainWindow::clearAllViews()
void MainWindow::showFormatReference()
{
static const QString referenceText = R"(
static const QString referenceText = R"UARTSCOPE(
# UARTScope Output Format Reference
# Give this text to your AI assistant to generate compatible UART output.
UARTScope reads from a serial UART port and interprets the incoming text in
three parallel ways. You can use any combination.
UARTScope reads from either a serial UART port OR a plain TCP connection
(see the "Netzwerk (TCP)" tab in the Connect dialog - handy for emulators
like FS-UAE whose debug UART is piped through socat to a TCP port) and
interprets the incoming text in three parallel ways. You can use any
combination.
1. RAW OUTPUT (always active)
@@ -397,7 +423,7 @@ void uart_status_update(void) {
// Clear screen from firmware when you want a fresh start:
// uart_printf("\033[2J\033[H");
)";
)UARTSCOPE";
auto *dlg = new QDialog(this);
dlg->setWindowTitle(tr("UARTScope Format Reference"));
@@ -546,7 +572,7 @@ void MainWindow::showAbout()
grid->addWidget(v, row, 1);
};
addRow(0, tr("Version"), "1.0.0");
addRow(0, tr("Version"), "1.1.0");
addRow(1, tr("Entwickler"), "Dany Thinnes");
addRow(2, tr("Projekt"), "Projekt Hirnfrei");
addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR));

View File

@@ -5,6 +5,7 @@
SerialWorker::SerialWorker(QObject *parent)
: QObject(parent)
, m_port(new QSerialPort(this))
, m_socket(new QTcpSocket(this))
, m_idleFlushTimer(new QTimer(this))
, m_reconnectTimer(new QTimer(this))
{
@@ -16,6 +17,11 @@ SerialWorker::SerialWorker(QObject *parent)
connect(m_port, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead);
connect(m_port, &QSerialPort::errorOccurred, this, &SerialWorker::onPortError);
connect(m_socket, &QTcpSocket::readyRead, this, &SerialWorker::onReadyRead);
connect(m_socket, &QTcpSocket::connected, this, &SerialWorker::onSocketConnected);
connect(m_socket, &QTcpSocket::disconnected, this, &SerialWorker::onSocketDisconnected);
connect(m_socket, &QTcpSocket::errorOccurred, this, &SerialWorker::onSocketError);
}
SerialWorker::~SerialWorker()
@@ -32,6 +38,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
QSerialPort::StopBits stopBits,
QSerialPort::FlowControl flowControl)
{
m_mode = ConnectionMode::Serial;
m_portName = portName;
m_baudRate = baudRate;
m_dataBits = dataBits;
@@ -41,6 +48,10 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
m_userDisconnected = false;
m_reconnectAttempt = 0;
// Make sure any previous connection (of either transport) is torn down
// before switching to serial mode.
if (m_socket->state() != QAbstractSocket::UnconnectedState)
m_socket->abort();
if (m_port->isOpen())
m_port->close();
@@ -57,6 +68,7 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
scheduleReconnect();
return;
}
m_device = m_port;
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
@@ -64,15 +76,54 @@ void SerialWorker::openPort(const QString &portName, qint32 baudRate,
emit portOpened();
}
void SerialWorker::openNetwork(const QString &host, quint16 port)
{
m_mode = ConnectionMode::Network;
m_networkHost = host;
m_networkPort = port;
m_userDisconnected = false;
m_reconnectAttempt = 0;
// Make sure any previous connection (of either transport) is torn down
// before switching to network mode.
if (m_port->isOpen())
m_port->close();
if (m_socket->state() != QAbstractSocket::UnconnectedState)
m_socket->abort();
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
// connectToHost() is asynchronous - the outcome (success or failure)
// arrives later via the connected()/errorOccurred() signals, handled
// in onSocketConnected()/onSocketError() below.
m_socket->connectToHost(host, port);
}
void SerialWorker::closePort()
{
m_userDisconnected = true;
m_reconnectTimer->stop();
m_idleFlushTimer->stop();
bool wasOpen = false;
if (m_port && m_port->isOpen()) {
m_port->close();
emit portClosed();
wasOpen = true;
}
if (m_socket && m_socket->state() != QAbstractSocket::UnconnectedState) {
// abort() tears the connection down immediately (no graceful
// shutdown handshake needed for a debug/monitor connection like
// this one) and reliably triggers disconnected() right away.
m_socket->abort();
wasOpen = true;
}
m_device = nullptr;
if (wasOpen)
emit portClosed();
stopLogging();
}
@@ -117,11 +168,14 @@ void SerialWorker::stopLogging()
void SerialWorker::onReadyRead()
{
processRawData(m_port->readAll());
if (m_device)
processRawData(m_device->readAll());
}
void SerialWorker::onPortError(QSerialPort::SerialPortError err)
{
if (m_mode != ConnectionMode::Serial)
return;
if (err == QSerialPort::NoError)
return;
@@ -130,6 +184,7 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
if (fatal) {
m_port->close();
m_device = nullptr;
emit portClosed();
if (m_autoReconnect && !m_userDisconnected) {
scheduleReconnect();
@@ -139,6 +194,52 @@ void SerialWorker::onPortError(QSerialPort::SerialPortError err)
}
}
void SerialWorker::onSocketConnected()
{
if (m_mode != ConnectionMode::Network)
return;
m_device = m_socket;
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
}
void SerialWorker::onSocketDisconnected()
{
if (m_mode != ConnectionMode::Network)
return;
m_device = nullptr;
// If this disconnect was requested by us (closePort()), that function
// has already emitted portClosed() and handles all cleanup - nothing
// more to do here, and reconnecting would be wrong.
if (m_userDisconnected)
return;
emit portClosed();
if (m_autoReconnect)
scheduleReconnect();
}
void SerialWorker::onSocketError(QAbstractSocket::SocketError)
{
if (m_mode != ConnectionMode::Network)
return;
const QString msg = m_socket->errorString();
m_device = nullptr;
if (m_autoReconnect && !m_userDisconnected) {
scheduleReconnect();
} else {
emit errorOccurred(msg);
}
}
void SerialWorker::tryReconnect()
{
if (m_userDisconnected)
@@ -147,21 +248,30 @@ void SerialWorker::tryReconnect()
++m_reconnectAttempt;
emit reconnecting(m_reconnectAttempt);
m_port->setPortName(m_portName);
m_port->setBaudRate(m_baudRate);
m_port->setDataBits(m_dataBits);
m_port->setParity(m_parity);
m_port->setStopBits(m_stopBits);
m_port->setFlowControl(m_flowControl);
if (m_mode == ConnectionMode::Serial) {
m_port->setPortName(m_portName);
m_port->setBaudRate(m_baudRate);
m_port->setDataBits(m_dataBits);
m_port->setParity(m_parity);
m_port->setStopBits(m_stopBits);
m_port->setFlowControl(m_flowControl);
if (m_port->open(QIODevice::ReadOnly)) {
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
if (m_port->open(QIODevice::ReadOnly)) {
m_device = m_port;
m_idleFlushTimer->stop();
m_buffer.clear();
m_scanTail.clear();
m_reconnectAttempt = 0;
emit portOpened();
} else {
scheduleReconnect();
}
} else {
scheduleReconnect();
// Network: connectToHost() is asynchronous. Success/failure is
// reported later via onSocketConnected()/onSocketError(), which
// either finish this attempt or schedule the next one.
m_socket->abort();
m_socket->connectToHost(m_networkHost, m_networkPort);
}
}

View File

@@ -7,6 +7,7 @@
#include <QStandardPaths>
#include <QMetaObject>
#include <QResizeEvent>
#include <QMessageBox>
static QStringList enumerateVideoDevices()
{
@@ -40,12 +41,16 @@ VideoWidget::VideoWidget(QWidget *parent)
m_running = false;
m_startBtn->setText(tr("▶ Start"));
m_infoLabel->setText(tr("Stopped"));
if (m_recording)
stopRecording();
update();
});
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
m_infoLabel->setText(tr("Error: %1").arg(msg));
m_running = false;
m_startBtn->setText(tr("▶ Start"));
if (m_recording)
stopRecording();
});
m_thread->start();
@@ -57,6 +62,9 @@ void VideoWidget::shutdown()
return;
m_shutdownDone = true;
if (m_recording)
stopRecording();
m_worker->stopCapture();
if (m_thread && m_thread->isRunning()) {
@@ -104,11 +112,18 @@ void VideoWidget::setupUi()
m_screenshotBtn->setMaximumWidth(32);
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
m_recordBtn = new QPushButton(tr("⏺ Record"), this);
m_recordBtn->setCheckable(true);
m_recordBtn->setMaximumWidth(80);
m_recordBtn->setToolTip(tr("Record the live preview to an MP4 file (requires ffmpeg)"));
connect(m_recordBtn, &QPushButton::toggled, this, &VideoWidget::onRecordToggled);
ctrlRow->addWidget(m_deviceCombo, 1);
ctrlRow->addWidget(refreshBtn);
ctrlRow->addWidget(m_startBtn);
ctrlRow->addWidget(m_freezeBtn);
ctrlRow->addWidget(m_screenshotBtn);
ctrlRow->addWidget(m_recordBtn);
layout->addLayout(ctrlRow);
m_infoLabel = new QLabel(tr("No device started"), this);
@@ -151,6 +166,14 @@ void VideoWidget::onScreenshot()
void VideoWidget::onNewFrame(const QImage &frame)
{
// Feed the recorder regardless of freeze state, so pausing the preview
// doesn't also pause/corrupt the recording - recording tracks the real
// incoming stream, not what's currently shown on screen.
if (m_recording && m_recordProcess &&
m_recordProcess->state() == QProcess::Running) {
writeRecordingFrame(frame);
}
if (m_frozen)
return;
m_frame = frame;
@@ -237,3 +260,134 @@ void VideoWidget::resizeEvent(QResizeEvent *event)
QWidget::resizeEvent(event);
updateScaled();
}
// ── Recording (raw frames piped into ffmpeg, encoded to MP4) ──────────────
void VideoWidget::onRecordToggled(bool checked)
{
if (checked)
startRecording();
else
stopRecording();
}
void VideoWidget::startRecording()
{
if (m_frame.isNull()) {
QMessageBox::warning(this, tr("No signal"),
tr("Start the video capture first before recording."));
m_recordBtn->setChecked(false);
return;
}
const QString defaultName =
QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)
+ "/uartscope_"
+ QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")
+ ".mp4";
const QString path = QFileDialog::getSaveFileName(
this, tr("Save recording as"), defaultName,
tr("MP4 Video (*.mp4)"));
if (path.isEmpty()) {
m_recordBtn->setChecked(false);
return;
}
// Frames are piped in as raw BGRA (matching QImage::Format_RGB32's
// in-memory byte layout on little-endian systems) at whatever
// resolution capture is currently running at; a resolution change
// mid-recording would corrupt ffmpeg's raw input stream, so
// writeRecordingFrame() watches for that and stops the recording if
// it happens (see there).
m_recordSize = m_frame.size();
m_recordProcess = new QProcess(this);
connect(m_recordProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
m_infoLabel->setText(tr("Recording error: %1").arg(m_recordProcess->errorString()));
stopRecording();
});
connect(m_recordProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this](int exitCode, QProcess::ExitStatus) {
if (exitCode != 0 && m_recordProcess)
m_infoLabel->setText(tr("ffmpeg exited with code %1").arg(exitCode));
if (m_recordProcess) {
m_recordProcess->deleteLater();
m_recordProcess = nullptr;
}
});
// -use_wallclock_as_timestamps: frames don't arrive at a strictly
// constant rate (V4L2 delivers whatever the device/driver produces),
// so we let ffmpeg timestamp each frame using the system clock at the
// moment it's read from the pipe rather than assuming a fixed
// framerate. That keeps the resulting MP4's real-time duration
// correct even if the capture rate varies or briefly stalls.
const QStringList args = {
"-y",
"-f", "rawvideo",
"-pixel_format", "bgra",
"-video_size", QStringLiteral("%1x%2").arg(m_recordSize.width()).arg(m_recordSize.height()),
"-use_wallclock_as_timestamps", "1",
"-i", "-",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-preset", "veryfast",
"-movflags", "+faststart",
path
};
m_recordProcess->start("ffmpeg", args);
if (!m_recordProcess->waitForStarted(3000)) {
QMessageBox::critical(this, tr("Recording failed"),
tr("Could not start ffmpeg. Is it installed and available on PATH?"));
m_recordProcess->deleteLater();
m_recordProcess = nullptr;
m_recordBtn->setChecked(false);
return;
}
m_recording = true;
m_recordBtn->setText(tr("⏹ Stop"));
m_infoLabel->setText(tr("Recording to %1").arg(path));
}
void VideoWidget::stopRecording()
{
m_recording = false;
m_recordBtn->setChecked(false);
m_recordBtn->setText(tr("⏺ Record"));
if (m_recordProcess) {
// Closing stdin signals ffmpeg there's no more input; it then
// finishes encoding/muxing and exits on its own. The finished()
// handler connected in startRecording() cleans the QProcess up
// once that happens.
m_recordProcess->closeWriteChannel();
if (!m_recordProcess->waitForFinished(3000))
m_recordProcess->terminate();
}
}
void VideoWidget::writeRecordingFrame(const QImage &frame)
{
if (frame.size() != m_recordSize) {
// The capture device changed resolution mid-recording (shouldn't
// normally happen, but e.g. a device re-negotiating format could
// trigger it). Feeding ffmpeg mismatched frame sizes on a raw pipe
// would desync/corrupt the output, so stop cleanly instead.
stopRecording();
m_infoLabel->setText(tr("Recording stopped: frame size changed"));
return;
}
// Format_RGB32 stores each pixel as 0xffRRGGBB in host byte order,
// which on little-endian systems (the overwhelming majority of
// desktop/embedded targets) is byte-for-byte BGRA - matching the
// "-pixel_format bgra" we told ffmpeg to expect above.
const QImage bgra = frame.convertToFormat(QImage::Format_RGB32);
m_recordProcess->write(reinterpret_cast<const char *>(bgra.constBits()),
static_cast<qint64>(bgra.sizeInBytes()));
}