- Grafische Ausgabe kann nun als Video aufgezeichnet werden
- Verbindung über TCP/IP
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user