- 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

@@ -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()));
}