178 lines
6.7 KiB
C++
178 lines
6.7 KiB
C++
#pragma once
|
|
#include <QWidget>
|
|
#include <QImage>
|
|
#include <QSize>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QComboBox>
|
|
#include <QHBoxLayout>
|
|
#include <QVBoxLayout>
|
|
#include <QThread>
|
|
#include <QProcess>
|
|
|
|
#include "v4l2worker.h"
|
|
|
|
// A uniform, icon-only push button used for the video toolbar (start/stop,
|
|
// freeze, screenshot, record, fullscreen, audio, ...). All toolbar buttons
|
|
// share the same fixed square size and font so the row lines up neatly
|
|
// instead of every button sizing itself to its own label.
|
|
QPushButton *makeToolbarIconButton(const QString &symbol, const QString &tooltip,
|
|
QWidget *parent);
|
|
|
|
// A borderless top-level window that mirrors the live video frame at full
|
|
// screen size. Opened from VideoWidget's "⛶" button; closes itself on
|
|
// Escape or a double-click anywhere on it.
|
|
class FullscreenVideoView : public QWidget
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit FullscreenVideoView(QWidget *parent = nullptr);
|
|
|
|
void setFrame(const QImage &frame);
|
|
|
|
signals:
|
|
// Emitted from closeEvent() so VideoWidget can drop its pointer before
|
|
// the WA_DeleteOnClose deletion actually happens.
|
|
void closed();
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *event) override;
|
|
void keyPressEvent(QKeyEvent *event) override;
|
|
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
|
void closeEvent(QCloseEvent *event) override;
|
|
|
|
private:
|
|
QImage m_frame;
|
|
};
|
|
|
|
// VideoWidget shows the live V4L2 preview and can additionally record it
|
|
// to an MP4 file. Recording works by piping raw BGRA frames into an
|
|
// external `ffmpeg` process (via QProcess/stdin), which encodes them to
|
|
// H.264/MP4 - this avoids reimplementing video encoding and just needs
|
|
// ffmpeg to be installed and on PATH.
|
|
class VideoWidget : public QWidget
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit VideoWidget(QWidget *parent = nullptr);
|
|
~VideoWidget();
|
|
|
|
public:
|
|
void shutdown(); // safe to call multiple times, called from MainWindow::closeEvent
|
|
// Folder that saveAutoScreenshot() writes into. Empty = not configured
|
|
// (saveAutoScreenshot() will report an error instead of silently
|
|
// dropping the request).
|
|
void setAutoScreenshotDir(const QString &dir) { m_autoScreenshotDir = dir; }
|
|
|
|
// Folders the manual "Screenshot"/"Record" buttons' save dialogs
|
|
// default to. Set by MainWindow from persisted settings on startup;
|
|
// VideoWidget reports back via lastScreenshotDirChanged()/
|
|
// lastVideoDirChanged() whenever the user picks a new folder so
|
|
// MainWindow can persist it for next time.
|
|
void setLastScreenshotDir(const QString &dir) { m_lastScreenshotDir = dir; }
|
|
void setLastVideoDir(const QString &dir) { m_lastVideoDir = dir; }
|
|
|
|
public slots:
|
|
void onNewFrame(const QImage &frame);
|
|
void clearFrame(); // blanks the preview (used on ANSI clear-screen)
|
|
// Saves whichever frame is currently in m_frame under `filename` inside
|
|
// m_autoScreenshotDir. Triggered by a [SCREENSHOT] control tag from the
|
|
// firmware - the point is to grab the frame that's already in flight
|
|
// the instant the tag arrives (no dialog, no user reaction time), e.g.
|
|
// to reliably catch a video frame showing a memory dump.
|
|
void saveAutoScreenshot(const QString &filename);
|
|
|
|
signals:
|
|
// Emitted after every saveAutoScreenshot() attempt so the caller (the
|
|
// main window) can surface a transient status message. Never blocks/
|
|
// pops a dialog itself, since this can fire in rapid succession from
|
|
// firmware-driven triggers.
|
|
void autoScreenshotResult(bool success, const QString &message);
|
|
|
|
// Emitted whenever the user successfully saves via the manual
|
|
// "Screenshot"/"Record" buttons, so MainWindow can persist the folder
|
|
// for next time (see setLastScreenshotDir()/setLastVideoDir()).
|
|
void lastScreenshotDirChanged(const QString &dir);
|
|
void lastVideoDirChanged(const QString &dir);
|
|
|
|
private slots:
|
|
void onStartStop();
|
|
void onFreeze(bool frozen);
|
|
void onScreenshot();
|
|
void onRecordToggled(bool checked);
|
|
void onFullscreenToggle();
|
|
void onAudioToggled(bool checked);
|
|
void refreshDeviceList();
|
|
void refreshAudioDevices();
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *event) override;
|
|
void resizeEvent(QResizeEvent *event) override;
|
|
|
|
private:
|
|
void setupUi();
|
|
void startCapture();
|
|
void stopCapture();
|
|
void updateScaled();
|
|
void startRecording();
|
|
void stopRecording();
|
|
void writeRecordingFrame(const QImage &frame);
|
|
void startAudio();
|
|
void stopAudio();
|
|
static QStringList enumerateAudioDevices();
|
|
|
|
QThread *m_thread = nullptr;
|
|
V4l2Worker *m_worker = nullptr;
|
|
|
|
QImage m_frame;
|
|
QImage m_scaled;
|
|
bool m_frozen = false;
|
|
bool m_running = false;
|
|
bool m_shutdownDone = false;
|
|
|
|
QComboBox *m_deviceCombo = nullptr;
|
|
QPushButton *m_startBtn = nullptr;
|
|
QPushButton *m_freezeBtn = nullptr;
|
|
QPushButton *m_screenshotBtn = nullptr;
|
|
QPushButton *m_recordBtn = nullptr;
|
|
QPushButton *m_fullscreenBtn = nullptr;
|
|
QLabel *m_infoLabel = nullptr;
|
|
|
|
// Audio passthrough: plays the grabber's ALSA capture device (e.g. the
|
|
// "USB Audio" card that accompanies a /dev/videoN HDMI/USB grabber)
|
|
// straight to the default output, the same way the HDMI viewer does it.
|
|
// Implemented by shelling out to `ffplay -f alsa -i <device>` with
|
|
// -nodisp (audio only, no extra window) - same approach as recording,
|
|
// just piping the other direction (device -> speakers instead of
|
|
// frames -> file).
|
|
QComboBox *m_audioCombo = nullptr;
|
|
QPushButton *m_audioBtn = nullptr;
|
|
QProcess *m_audioProcess = nullptr;
|
|
bool m_audioActive = false;
|
|
|
|
// Fullscreen overlay window (nullptr when not open). Owns itself via
|
|
// Qt::WA_DeleteOnClose; VideoWidget just tracks whether one currently
|
|
// exists and forwards frames to it while it's open.
|
|
FullscreenVideoView *m_fullscreenView = nullptr;
|
|
|
|
// Recording state. m_recordProcess is the running ffmpeg instance (or
|
|
// nullptr when not recording); m_recordSize is the frame size the
|
|
// recording was started with, so a mid-stream resolution change (which
|
|
// ffmpeg's raw pipe can't handle) can be detected and the recording
|
|
// stopped cleanly instead of corrupting the output file.
|
|
QProcess *m_recordProcess = nullptr;
|
|
bool m_recording = false;
|
|
QSize m_recordSize;
|
|
|
|
// Folder for saveAutoScreenshot() ([SCREENSHOT] tag). Set via
|
|
// setAutoScreenshotDir(), persisted/restored by MainWindow.
|
|
QString m_autoScreenshotDir;
|
|
|
|
// Folders the manual Screenshot/Record save dialogs default to (see
|
|
// setLastScreenshotDir()/setLastVideoDir() above).
|
|
QString m_lastScreenshotDir;
|
|
QString m_lastVideoDir;
|
|
};
|