Erster Teststream absolviert. Positives Ergebnis

This commit is contained in:
2026-09-05 18:55:56 +02:00
parent 953c37d8d1
commit cc3527f86a
11 changed files with 1198 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(HDMIViewer VERSION 1.3.2 LANGUAGES CXX) project(HDMIViewer VERSION 1.4.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -8,7 +8,8 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
# Qt6 Gui enthält den JPEG-Plugin-Support (QImage::fromData) # Qt6 Gui enthält den JPEG-Plugin-Support (QImage::fromData)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) # OpenGLWidgets für das GPU-beschleunigte Video-Rendering (Bildglättung/Retro-Optik)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets)
# ALSA # ALSA
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
@@ -37,6 +38,8 @@ target_link_libraries(hdmi-viewer
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
Qt6::OpenGL
Qt6::OpenGLWidgets
${ALSA_LIBRARIES} ${ALSA_LIBRARIES}
) )

View File

@@ -14,6 +14,32 @@ Gedacht als Companion zu OBS Studio: **Anzeigen ohne Aufnehmen**.
- Titelleiste zeigt tatsächliche Auflösung und FPS - Titelleiste zeigt tatsächliche Auflösung und FPS
- Colorspace-Konvertierung im ThreadPool (kein Ruckeln) - Colorspace-Konvertierung im ThreadPool (kein Ruckeln)
- FPS explizit auf 60fps gesetzt (was Grabber liefert wird in Statusbar angezeigt) - FPS explizit auf 60fps gesetzt (was Grabber liefert wird in Statusbar angezeigt)
- **Bildglättung** (standardmäßig an): GPU-Shader trennt Luma/Chroma und
behandelt sie getrennt Chroma wird stärker weichgezeichnet (killt
Dot-Crawl/Farbrauschen billiger Composite→HDMI-Scaler wie Lioncast Mini
AV2HDMI o.ä.), Luma wird leicht geglättet und dezent gegengeschärft
(nimmt harte Scaler-Kanten raus, ohne matschig zu wirken). Keine Optik-
Spielerei, sondern eine echte Bildverbesserung. Taste **S** zum Umschalten.
- **Retro-Optik** (standardmäßig aus): separater Nostalgie-Look Scanlines,
Phosphor-/Aperture-Grille-Maske, leichte Wölbung wie bei einem alten
Röhrenfernseher. Lässt sich mit der Bildglättung kombinieren. Taste **C**.
- **Helligkeit** (Tasten **,** dunkler / **.** heller): Gamma-Anpassung, live
und persistiert. Sinnvoll als Feinjustierung, falls Farbraum-Erkennung
(siehe unten) allein nicht perfekt zum eigenen Bildschirm passt.
- **Hardware-Helligkeit**: Beim Öffnen des Geräts wird (falls vom Chip
unterstützt) der V4L2-Brightness-Regler direkt am Grabber moderat
angehoben (+20 % des Wertebereichs ab Werkseinstellung) das wirkt
bereits VOR der JPEG-Kompression und kann so tatsächlich verlorene
Schattendetails retten, statt nur nachträglich zu strecken, was von der
Kompression übrig blieb. Diagnose-Ausgabe der verfügbaren Regler landet
beim Start im Terminal.
- **Studio-/Full-Range-Korrektur**: Das fertig dekodierte Bild wird von einem
komprimierten Kontrastumfang (Schwarzpunkt niedrig angesetzt, Weißpunkt
≈235/255) auf den vollen 0255-Kontrastumfang gedehnt. Ohne diese
Korrektur wirkt das Bild dumpf, kontrastarm und zu dunkel viele billige
HDMI-Grabber liefern (oder kodieren, auch bei MJPEG) genau in diesem
komprimierten Bereich. Greift direkt im GPU-Shader auf dem fertigen
RGB-Bild, also unabhängig vom Pixelformat.
## Abhängigkeiten (Arch Linux) ## Abhängigkeiten (Arch Linux)
@@ -21,6 +47,9 @@ Gedacht als Companion zu OBS Studio: **Anzeigen ohne Aufnehmen**.
sudo pacman -S qt6-base cmake alsa-lib linux-headers base-devel sudo pacman -S qt6-base cmake alsa-lib linux-headers base-devel
``` ```
`qt6-base` bringt auf Arch bereits die benötigten `Qt6OpenGL`- und
`Qt6OpenGLWidgets`-Module mit.
## Bauen ## Bauen
```bash ```bash
@@ -40,6 +69,9 @@ make -j$(nproc)
| Taste | Funktion | | Taste | Funktion |
|--------------|---------------------------------------------| |--------------|---------------------------------------------|
| F | Vollbild ein/aus | | F | Vollbild ein/aus |
| S | Bildglättung ein/aus |
| C | Retro-Optik (Scanlines/Maske) ein/aus |
| , / . | Dunkler / Heller (Helligkeit, gespeichert) |
| Esc | Vollbild verlassen ODER zurück zum Dialog | | Esc | Vollbild verlassen ODER zurück zum Dialog |
## Einstellungen ## Einstellungen

View File

@@ -1,6 +1,10 @@
#include "CaptureThread.h" #include "CaptureThread.h"
#include <QDebug> #include <QDebug>
#include <QSettings>
#include <QFile>
#include <QDir>
#include <QDateTime>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
@@ -16,6 +20,48 @@ static int xioctl(int fd, unsigned long request, void* arg)
return r; return r;
} }
// Listet alle vom Grabber-Chip unterstützten V4L2-Regler (Helligkeit,
// Kontrast, Gain, Belichtung, ...) mit ihrem Wertebereich und aktuellen Wert
// ins Terminal. Rein diagnostisch zeigt, ob/welche Hardware-Verstärkung
// VOR der JPEG-Kompression überhaupt ansetzbar wäre, bevor wir dafür UI
// bauen. Manche Chips unterstützen keinen dieser Regler; dann bleibt die
// Liste leer, was ebenfalls eine wichtige Information ist.
static void logAvailableControls(int fd)
{
qDebug().noquote() << "── V4L2-Regler des Grabbers ──────────────────────";
v4l2_queryctrl qc{};
qc.id = V4L2_CTRL_FLAG_NEXT_CTRL;
bool any = false;
while (xioctl(fd, VIDIOC_QUERYCTRL, &qc) == 0) {
if (!(qc.flags & V4L2_CTRL_FLAG_DISABLED) &&
qc.type != V4L2_CTRL_TYPE_CTRL_CLASS) {
any = true;
v4l2_control ctrl{};
ctrl.id = qc.id;
const bool gotCurrent = (xioctl(fd, VIDIOC_G_CTRL, &ctrl) == 0);
qDebug().noquote() << QString(
" %1 (id=0x%2) min=%3 max=%4 step=%5 default=%6 aktuell=%7")
.arg(QString::fromLatin1(reinterpret_cast<const char*>(qc.name)), -28)
.arg(qc.id, 0, 16)
.arg(qc.minimum)
.arg(qc.maximum)
.arg(qc.step)
.arg(qc.default_value)
.arg(gotCurrent ? QString::number(ctrl.value) : QStringLiteral("?"));
}
qc.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
}
if (!any)
qDebug().noquote() << " (keine regelbaren Controls gemeldet)";
qDebug().noquote() << "───────────────────────────────────────────────────";
}
// ── YUYV → RGB888 (BT.601 fixed-point) ────────────────────────────────────── // ── YUYV → RGB888 (BT.601 fixed-point) ──────────────────────────────────────
static inline quint8 clamp255(int v) static inline quint8 clamp255(int v)
@@ -40,6 +86,11 @@ void CaptureThread::stopAndWait()
wait(); wait();
} }
void CaptureThread::requestFrameDump()
{
m_dumpRequested = true;
}
void CaptureThread::run() void CaptureThread::run()
{ {
if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) { if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) {
@@ -117,6 +168,10 @@ bool CaptureThread::openDevice()
emit errorOccurred(m_device + " unterstützt kein Streaming"); emit errorOccurred(m_device + " unterstützt kein Streaming");
return false; return false;
} }
logAvailableControls(m_fd);
applyHardwareControls();
return true; return true;
} }
@@ -273,14 +328,131 @@ void CaptureThread::closeDevice()
m_buffers.clear(); m_buffers.clear();
if (m_fd != -1) { if (m_fd != -1) {
restoreHardwareControls();
::close(m_fd); ::close(m_fd);
m_fd = -1; m_fd = -1;
qDebug() << "V4L2 freigegeben:" << m_device; qDebug() << "V4L2 freigegeben:" << m_device;
} }
} }
// Setzt beim Start Helligkeit/Kontrast/Sättigung des Grabber-Chips auf die
// vom Nutzer konfigurierten Werte (persistiert in QSettings, per Tasten
// oder Startdialog gesetzt) VOR der JPEG-Kompression, was tatsächlich
// Schattendetails vor dem verlustbehafteten Encoding retten kann. Merkt
// sich dabei den JEWEILS AKTUELLEN Wert jedes Reglers (der Ausgangszustand,
// egal von wem/wann gesetzt), um ihn beim Beenden wiederherzustellen so
// hinterlässt hdmi-viewer den Chip nicht dauerhaft verändert für andere
// Programme. Regler ohne gespeicherten Nutzerwert (settings.contains ==
// false) werden gar nicht erst angefasst.
void CaptureThread::applyHardwareControls()
{
QSettings settings;
auto handle = [&](quint32 id, const char* key, int& restoreOut) {
v4l2_queryctrl qc{};
qc.id = id;
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
return;
v4l2_control cur{};
cur.id = id;
if (xioctl(m_fd, VIDIOC_G_CTRL, &cur) == 0)
restoreOut = cur.value;
if (!settings.contains(key))
return; // kein expliziter Nutzerwert -> unangetastet lassen
const int target = qBound(qc.minimum, settings.value(key).toInt(), qc.maximum);
v4l2_control ctrl{};
ctrl.id = id;
ctrl.value = target;
if (xioctl(m_fd, VIDIOC_S_CTRL, &ctrl) == 0) {
qDebug().noquote() << QString("%1 gesetzt: %2 (Ausgangswert %3 gemerkt)")
.arg(key).arg(target).arg(restoreOut);
}
};
handle(V4L2_CID_BRIGHTNESS, "hardware/brightness", m_restoreBrightness);
handle(V4L2_CID_CONTRAST, "hardware/contrast", m_restoreContrast);
handle(V4L2_CID_SATURATION, "hardware/saturation", m_restoreSaturation);
}
void CaptureThread::restoreHardwareControls()
{
auto restore = [&](quint32 id, int value) {
if (value < 0) return;
v4l2_control ctrl{};
ctrl.id = id;
ctrl.value = value;
xioctl(m_fd, VIDIOC_S_CTRL, &ctrl);
};
restore(V4L2_CID_BRIGHTNESS, m_restoreBrightness);
restore(V4L2_CID_CONTRAST, m_restoreContrast);
restore(V4L2_CID_SATURATION, m_restoreSaturation);
qDebug().noquote() << "Hardware-Regler auf Ausgangswerte zurückgesetzt.";
}
// Für Live-Tasten (Kontrast/Sättigung): liest den aktuellen Wert, addiert
// delta, schreibt zurück, merkt sich den neuen Wert dauerhaft und meldet
// ihn per Signal (für die Einblendung in der UI).
void CaptureThread::adjustHardwareControl(quint32 id, const char* settingsKey,
const QString& label, int delta)
{
if (m_fd == -1) return;
v4l2_queryctrl qc{};
qc.id = id;
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
return;
v4l2_control cur{};
cur.id = id;
if (xioctl(m_fd, VIDIOC_G_CTRL, &cur) == -1)
return;
const int next = qBound(qc.minimum, cur.value + delta, qc.maximum);
v4l2_control ctrl{};
ctrl.id = id;
ctrl.value = next;
if (xioctl(m_fd, VIDIOC_S_CTRL, &ctrl) == 0) {
QSettings settings;
settings.setValue(settingsKey, next);
emit hardwareControlChanged(label, next, qc.minimum, qc.maximum);
}
}
void CaptureThread::adjustHardwareContrast(int delta)
{
adjustHardwareControl(V4L2_CID_CONTRAST, "hardware/contrast", "Hardware-Kontrast", delta);
}
void CaptureThread::adjustHardwareSaturation(int delta)
{
adjustHardwareControl(V4L2_CID_SATURATION, "hardware/saturation", "Hardware-Sättigung", delta);
}
void CaptureThread::processFrame(const void* data, size_t size) void CaptureThread::processFrame(const void* data, size_t size)
{ {
// Diagnose: rohes MJPEG-Frame unverändert (Byte für Byte, wie vom Chip
// geliefert) auf die Platte speichern, BEVOR irgendeine Dekodierung/
// Verarbeitung stattfindet. Lässt sich in einem unabhängigen Bildbe-
// trachter öffnen, um zu prüfen, ob die Rohdaten selbst schon dunkel
// sind oder ob die Abweichung erst bei uns beim Dekodieren entsteht.
if (m_dumpRequested.exchange(false) && m_pixelFormat == V4L2_PIX_FMT_MJPEG) {
const QString path = QDir::homePath() + "/hdmiviewer_frame_dump_" +
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".jpg";
QFile f(path);
if (f.open(QIODevice::WriteOnly)) {
f.write(reinterpret_cast<const char*>(data), static_cast<qint64>(size));
f.close();
qDebug().noquote() << "Rohes Frame gespeichert:" << path;
emit frameDumped(path);
} else {
qDebug().noquote() << "Frame-Dump fehlgeschlagen: Datei konnte nicht geschrieben werden.";
}
}
QImage img; QImage img;
switch (m_pixelFormat) { switch (m_pixelFormat) {
case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break; case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break;

View File

@@ -23,6 +23,9 @@ public:
~CaptureThread() override; ~CaptureThread() override;
void stopAndWait(); void stopAndWait();
void requestFrameDump(); // speichert das nächste rohe MJPEG-Frame unverändert auf die Platte
void adjustHardwareContrast(int delta);
void adjustHardwareSaturation(int delta);
int captureWidth() const { return m_width; } int captureWidth() const { return m_width; }
int captureHeight() const { return m_height; } int captureHeight() const { return m_height; }
@@ -32,6 +35,8 @@ signals:
void frameReady(QImage image); void frameReady(QImage image);
void errorOccurred(const QString& message); void errorOccurred(const QString& message);
void captureInfo(int width, int height, int fps, const QString& format); void captureInfo(int width, int height, int fps, const QString& format);
void frameDumped(QString path);
void hardwareControlChanged(QString label, int value, int minVal, int maxVal);
protected: protected:
void run() override; void run() override;
@@ -45,6 +50,9 @@ private:
void closeDevice(); void closeDevice();
bool initMmap(); bool initMmap();
void processFrame(const void* data, size_t size); void processFrame(const void* data, size_t size);
void applyHardwareControls(); // beim Start: eigene Werte setzen, Ausgangswerte merken
void restoreHardwareControls(); // beim Beenden: Ausgangswerte wiederherstellen
void adjustHardwareControl(quint32 id, const char* settingsKey, const QString& label, int delta);
// Dekodierung // Dekodierung
QImage decodeYUYV(const void* data) const; QImage decodeYUYV(const void* data) const;
@@ -60,4 +68,11 @@ private:
quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG; quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG;
std::vector<Buffer> m_buffers; std::vector<Buffer> m_buffers;
std::atomic<bool> m_running{false}; std::atomic<bool> m_running{false};
std::atomic<bool> m_dumpRequested{false};
// Ausgangswerte der Hardware-Regler (vor unseren Änderungen), für die
// Wiederherstellung beim Beenden. -1 = nicht gelesen/nicht verfügbar.
int m_restoreBrightness = -1;
int m_restoreContrast = -1;
int m_restoreSaturation = -1;
}; };

View File

@@ -1,9 +1,13 @@
#include "DeviceDialog.h" #include "DeviceDialog.h"
#include "VideoWidget.h" // für kBrightnessMin/Max, kSharpenMin/Max (eine Quelle der Wahrheit)
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QFormLayout> #include <QFormLayout>
#include <QComboBox> #include <QComboBox>
#include <QCheckBox>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QDir> #include <QDir>
@@ -51,11 +55,79 @@ DeviceDialog::DeviceDialog(QWidget* parent)
form->addRow("Audio-Gerät (ALSA):", m_audioCombo); form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
form->addRow("Ziel-Framerate:", m_fpsCombo); form->addRow("Ziel-Framerate:", m_fpsCombo);
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
m_smoothCheck->setChecked(true);
m_smoothCheck->setToolTip(
"Reduziert live Kompressions-/Scaler-Artefakte deines HDMI-Upscalers:\n"
"Farbrauschen/Dot-Crawl wird weicher gezeichnet, harte blockige\n"
"Pixelkanten werden geglättet und dezent gegengeschärft.\n"
"Das ist keine Optik-Spielerei, sondern eine Bildverbesserung \n"
"empfohlen für Composite-Quellen wie PS1/PS2 an billigen Scalern.\n"
"Umschaltbar im laufenden Betrieb mit [S].");
form->addRow("", m_smoothCheck);
m_retroCheck = new QCheckBox("Retro-Optik (Scanlines/Maske) aktivieren", this);
m_retroCheck->setToolTip(
"Rein optischer Nostalgie-Look: Scanlines, Phosphor-Maske,\n"
"leichte Wölbung wie bei einem alten Röhrenfernseher.\n"
"Lässt sich mit der Bildglättung kombinieren.\n"
"Umschaltbar im laufenden Betrieb mit [C].");
form->addRow("", m_retroCheck);
m_sharpenSpin = new QDoubleSpinBox(this);
m_sharpenSpin->setRange(VideoWidget::kSharpenMin, VideoWidget::kSharpenMax);
m_sharpenSpin->setSingleStep(VideoWidget::kSharpenStep);
m_sharpenSpin->setDecimals(1);
m_sharpenSpin->setValue(0.0);
m_sharpenSpin->setSpecialValueText("aus");
m_sharpenSpin->setToolTip(
"Unsharp Mask übers gesamte Bild, unabhängig von der Bildglättung.\n"
"0 = aus. Im laufenden Betrieb mit [X]/[+]/[-] änderbar.");
form->addRow("Nachschärfen:", m_sharpenSpin);
m_brightnessSpin = new QDoubleSpinBox(this);
m_brightnessSpin->setRange(VideoWidget::kBrightnessMin * 100.0, VideoWidget::kBrightnessMax * 100.0);
m_brightnessSpin->setSingleStep(VideoWidget::kBrightnessStep * 100.0);
m_brightnessSpin->setDecimals(0);
m_brightnessSpin->setSuffix(" %");
m_brightnessSpin->setValue(100.0);
m_brightnessSpin->setToolTip(
"Gamma-Helligkeit, 100 % = unverändert. Im laufenden Betrieb\n"
"mit [,]/[.] änderbar.");
form->addRow("Helligkeit:", m_brightnessSpin);
m_hwContrastSpin = new QSpinBox(this);
m_hwContrastSpin->setRange(-1, 100);
m_hwContrastSpin->setValue(-1);
m_hwContrastSpin->setSpecialValueText("unverändert");
m_hwContrastSpin->setToolTip(
"Hardware-Kontrast direkt am Grabber-Chip, VOR der JPEG-Kompression\n"
"(Wertebereich hängt vom jeweiligen Chip ab, hier 0-100 angenommen).\n"
"\"unverändert\" lässt den aktuellen Chip-Wert einfach in Ruhe.\n"
"Im laufenden Betrieb mit [1]/[2] änderbar. hdmi-viewer merkt sich\n"
"beim Start den Ausgangswert und stellt ihn beim Beenden wieder her.");
form->addRow("Hardware-Kontrast:", m_hwContrastSpin);
m_hwSaturationSpin = new QSpinBox(this);
m_hwSaturationSpin->setRange(-1, 100);
m_hwSaturationSpin->setValue(-1);
m_hwSaturationSpin->setSpecialValueText("unverändert");
m_hwSaturationSpin->setToolTip(
"Hardware-Sättigung direkt am Grabber-Chip, VOR der JPEG-Kompression.\n"
"\"unverändert\" lässt den aktuellen Chip-Wert einfach in Ruhe.\n"
"Im laufenden Betrieb mit [3]/[4] änderbar. hdmi-viewer merkt sich\n"
"beim Start den Ausgangswert und stellt ihn beim Beenden wieder her.");
form->addRow("Hardware-Sättigung:", m_hwSaturationSpin);
auto* hint = new QLabel( auto* hint = new QLabel(
"<small><b>Hinweis:</b> Der Grabber liefert die gewünschte FPS-Zahl nur wenn " "<small><b>Hinweis:</b> Der Grabber liefert die gewünschte FPS-Zahl nur wenn "
"genug USB-Bandbreite vorhanden ist. Die tatsächliche Rate wird in der " "genug USB-Bandbreite vorhanden ist. Die tatsächliche Rate wird in der "
"Titelleiste angezeigt.<br>" "Titelleiste angezeigt.<br>"
"<b>F</b> = Vollbild &nbsp;·&nbsp; <b>Esc</b> = zurück hierher</small>", "<b>F</b> Vollbild &nbsp;·&nbsp; <b>S</b> Glättung &nbsp;·&nbsp; "
"<b>C</b> Retro &nbsp;·&nbsp; <b>X/+/-</b> Schärfen &nbsp;·&nbsp; "
"<b>,/.</b> Helligkeit &nbsp;·&nbsp; <b>1/2</b> HW-Kontrast &nbsp;·&nbsp; "
"<b>3/4</b> HW-Sättigung &nbsp;·&nbsp; <b>V</b> Vergleich &nbsp;·&nbsp; "
"<b>P</b> Screenshot &nbsp;·&nbsp; <b>Esc</b> zurück hierher</small>",
this); this);
hint->setWordWrap(true); hint->setWordWrap(true);
@@ -81,10 +153,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
QString DeviceDialog::selectedVideoDevice() const { return m_videoCombo->currentData().toString(); } QString DeviceDialog::selectedVideoDevice() const { return m_videoCombo->currentData().toString(); }
QString DeviceDialog::selectedAudioDevice() const { return m_audioCombo->currentData().toString(); } QString DeviceDialog::selectedAudioDevice() const { return m_audioCombo->currentData().toString(); }
int DeviceDialog::selectedFps() const { return m_fpsCombo->currentData().toInt(); } int DeviceDialog::selectedFps() const { return m_fpsCombo->currentData().toInt(); }
bool DeviceDialog::selectedSmoothMode() const { return m_smoothCheck->isChecked(); }
bool DeviceDialog::selectedRetroMode() const { return m_retroCheck->isChecked(); }
float DeviceDialog::selectedSharpenAmount() const { return static_cast<float>(m_sharpenSpin->value()); }
float DeviceDialog::selectedBrightness() const { return static_cast<float>(m_brightnessSpin->value() / 100.0); }
int DeviceDialog::selectedHwContrast() const { return m_hwContrastSpin->value(); }
int DeviceDialog::selectedHwSaturation() const { return m_hwSaturationSpin->value(); }
void DeviceDialog::setPreselectedVideo(const QString& device) { selectByData(m_videoCombo, device); } void DeviceDialog::setPreselectedVideo(const QString& device) { selectByData(m_videoCombo, device); }
void DeviceDialog::setPreselectedAudio(const QString& device) { selectByData(m_audioCombo, device); } void DeviceDialog::setPreselectedAudio(const QString& device) { selectByData(m_audioCombo, device); }
void DeviceDialog::setPreselectedFps(int fps) { selectByData(m_fpsCombo, QString::number(fps)); } void DeviceDialog::setPreselectedFps(int fps) { selectByData(m_fpsCombo, QString::number(fps)); }
void DeviceDialog::setPreselectedSmoothMode(bool enabled) { m_smoothCheck->setChecked(enabled); }
void DeviceDialog::setPreselectedRetroMode(bool enabled) { m_retroCheck->setChecked(enabled); }
void DeviceDialog::setPreselectedSharpenAmount(float amount) { m_sharpenSpin->setValue(amount); }
void DeviceDialog::setPreselectedBrightness(float gamma) { m_brightnessSpin->setValue(gamma * 100.0); }
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
void DeviceDialog::selectByData(QComboBox* combo, const QString& data) void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
{ {

View File

@@ -5,6 +5,9 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QComboBox; class QComboBox;
class QCheckBox;
class QDoubleSpinBox;
class QSpinBox;
QT_END_NAMESPACE QT_END_NAMESPACE
class DeviceDialog : public QDialog class DeviceDialog : public QDialog
@@ -17,10 +20,22 @@ public:
QString selectedVideoDevice() const; QString selectedVideoDevice() const;
QString selectedAudioDevice() const; QString selectedAudioDevice() const;
int selectedFps() const; int selectedFps() const;
bool selectedSmoothMode() const;
bool selectedRetroMode() const;
float selectedSharpenAmount() const;
float selectedBrightness() const;
int selectedHwContrast() const; // -1 = unverändert lassen
int selectedHwSaturation() const; // -1 = unverändert lassen
void setPreselectedVideo(const QString& device); void setPreselectedVideo(const QString& device);
void setPreselectedAudio(const QString& device); void setPreselectedAudio(const QString& device);
void setPreselectedFps(int fps); void setPreselectedFps(int fps);
void setPreselectedSmoothMode(bool enabled);
void setPreselectedRetroMode(bool enabled);
void setPreselectedSharpenAmount(float amount);
void setPreselectedBrightness(float gamma);
void setPreselectedHwContrast(int value); // -1 = unverändert
void setPreselectedHwSaturation(int value); // -1 = unverändert
private: private:
void populateVideoDevices(); void populateVideoDevices();
@@ -30,4 +45,10 @@ private:
QComboBox* m_videoCombo = nullptr; QComboBox* m_videoCombo = nullptr;
QComboBox* m_audioCombo = nullptr; QComboBox* m_audioCombo = nullptr;
QComboBox* m_fpsCombo = nullptr; QComboBox* m_fpsCombo = nullptr;
QCheckBox* m_smoothCheck = nullptr;
QCheckBox* m_retroCheck = nullptr;
QDoubleSpinBox* m_sharpenSpin = nullptr;
QDoubleSpinBox* m_brightnessSpin = nullptr;
QSpinBox* m_hwContrastSpin = nullptr;
QSpinBox* m_hwSaturationSpin = nullptr;
}; };

View File

@@ -9,18 +9,28 @@
#include <QMenuBar> #include <QMenuBar>
#include <QMenu> #include <QMenu>
#include <QAction> #include <QAction>
#include <QSettings>
#include <QDialog> #include <QDialog>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QFrame> #include <QFrame>
#include <QTimer>
#include <QDir>
#include <QDateTime>
MainWindow::MainWindow(const QString& videoDevice, MainWindow::MainWindow(const QString& videoDevice,
const QString& audioDevice, const QString& audioDevice,
int targetFps, int targetFps,
bool smoothModeEnabled,
bool retroModeEnabled,
float brightness,
float sharpenAmount,
float lastSharpenAmount,
QWidget* parent) QWidget* parent)
: QMainWindow(parent) : QMainWindow(parent)
, m_lastSharpenAmount(lastSharpenAmount)
, m_videoDevice(videoDevice) , m_videoDevice(videoDevice)
, m_audioDevice(audioDevice) , m_audioDevice(audioDevice)
{ {
@@ -28,8 +38,20 @@ MainWindow::MainWindow(const QString& videoDevice,
resize(1280, 720); resize(1280, 720);
m_video = new VideoWidget(this); m_video = new VideoWidget(this);
m_video->setSmoothModeEnabled(smoothModeEnabled);
m_video->setRetroModeEnabled(retroModeEnabled);
m_video->setSharpenAmount(sharpenAmount);
m_video->setBrightness(brightness);
setCentralWidget(m_video); setCentralWidget(m_video);
// Kurzzeitige Einblendung unten links (funktioniert auch im Vollbild,
// wo Menü- und Statusleiste ausgeblendet sind).
m_overlayLabel = new QLabel(m_video);
m_overlayLabel->setStyleSheet(
"background-color: rgba(20, 20, 20, 200); color: white;"
"padding: 6px 12px; border-radius: 4px; font-size: 13px;");
m_overlayLabel->hide();
setupMenuBar(); setupMenuBar();
updateStatusBar(); updateStatusBar();
@@ -45,6 +67,12 @@ MainWindow::MainWindow(const QString& videoDevice,
connect(m_capture, &CaptureThread::captureInfo, connect(m_capture, &CaptureThread::captureInfo,
this, &MainWindow::onCaptureInfo, this, &MainWindow::onCaptureInfo,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(m_capture, &CaptureThread::frameDumped,
this, &MainWindow::onFrameDumped,
Qt::QueuedConnection);
connect(m_capture, &CaptureThread::hardwareControlChanged,
this, &MainWindow::onHardwareControlChanged,
Qt::QueuedConnection);
m_capture->start(); m_capture->start();
@@ -65,6 +93,54 @@ MainWindow::~MainWindow()
void MainWindow::setupMenuBar() void MainWindow::setupMenuBar()
{ {
QMenu* viewMenu = menuBar()->addMenu("&Anzeige");
m_smoothAction = viewMenu->addAction("Bildglättung\tS");
m_smoothAction->setCheckable(true);
m_smoothAction->setChecked(m_video->smoothModeEnabled());
connect(m_smoothAction, &QAction::triggered, this, &MainWindow::toggleSmoothMode);
m_retroAction = viewMenu->addAction("Retro-Optik (Scanlines/Maske)\tC");
m_retroAction->setCheckable(true);
m_retroAction->setChecked(m_video->retroModeEnabled());
connect(m_retroAction, &QAction::triggered, this, &MainWindow::toggleRetroMode);
m_sharpenAction = viewMenu->addAction("Nachschärfen\tX");
m_sharpenAction->setCheckable(true);
m_sharpenAction->setChecked(m_video->sharpenAmount() > 0.0f);
connect(m_sharpenAction, &QAction::triggered, this, &MainWindow::toggleSharpen);
QAction* sharpenUpAct = viewMenu->addAction("Schärfen +\t+");
connect(sharpenUpAct, &QAction::triggered, this, &MainWindow::increaseSharpen);
QAction* sharpenDownAct = viewMenu->addAction("Schärfen -\t-");
connect(sharpenDownAct, &QAction::triggered, this, &MainWindow::decreaseSharpen);
viewMenu->addSeparator();
QAction* brighterAct = viewMenu->addAction("Heller\t.");
connect(brighterAct, &QAction::triggered, this, &MainWindow::increaseBrightness);
QAction* darkerAct = viewMenu->addAction("Dunkler\t,");
connect(darkerAct, &QAction::triggered, this, &MainWindow::decreaseBrightness);
viewMenu->addSeparator();
QAction* rawAct = viewMenu->addAction("Roh-Modus (Diagnose)\tR");
rawAct->setCheckable(true);
rawAct->setChecked(m_video->rawEnabled());
connect(rawAct, &QAction::triggered, this, &MainWindow::toggleRaw);
QAction* compareAct = viewMenu->addAction("Live-Vergleich (roh/bearbeitet)\tV");
compareAct->setCheckable(true);
compareAct->setChecked(m_video->compareEnabled());
connect(compareAct, &QAction::triggered, this, &MainWindow::toggleCompare);
viewMenu->addSeparator();
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
QMenu* helpMenu = menuBar()->addMenu("&Hilfe"); QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer"); QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
@@ -72,11 +148,155 @@ void MainWindow::setupMenuBar()
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout); connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
} }
void MainWindow::showOverlayMessage(const QString& text)
{
if (!m_overlayLabel) return;
m_overlayLabel->setText(text);
m_overlayLabel->adjustSize();
const int margin = 16;
m_overlayLabel->move(margin, m_video->height() - m_overlayLabel->height() - margin);
m_overlayLabel->show();
m_overlayLabel->raise();
QTimer::singleShot(1500, m_overlayLabel, &QLabel::hide);
}
void MainWindow::toggleSmoothMode()
{
const bool enabled = !m_video->smoothModeEnabled();
m_video->setSmoothModeEnabled(enabled);
if (m_smoothAction) m_smoothAction->setChecked(enabled);
QSettings settings;
settings.setValue("display/smooth", enabled);
statusBar()->showMessage(
enabled ? "Bildglättung aktiviert" : "Bildglättung deaktiviert", 2000);
showOverlayMessage(enabled ? "Bildglättung: an" : "Bildglättung: aus");
}
void MainWindow::toggleRetroMode()
{
const bool enabled = !m_video->retroModeEnabled();
m_video->setRetroModeEnabled(enabled);
if (m_retroAction) m_retroAction->setChecked(enabled);
QSettings settings;
settings.setValue("display/retro", enabled);
statusBar()->showMessage(
enabled ? "Retro-Optik aktiviert" : "Retro-Optik deaktiviert", 2000);
showOverlayMessage(enabled ? "Retro-Optik: an" : "Retro-Optik: aus");
}
void MainWindow::toggleSharpen()
{
// Quick-Toggle: aus (0) ↔ zuletzt benutzter Wert (nicht der feste
// Standardwert!) merkt sich also z. B. 7.0, nicht immer 2.5.
if (m_video->sharpenAmount() > 0.0f) {
m_lastSharpenAmount = m_video->sharpenAmount();
applySharpen(0.0f);
} else {
applySharpen(m_lastSharpenAmount > 0.0f ? m_lastSharpenAmount : VideoWidget::kSharpenDefault);
}
}
void MainWindow::increaseSharpen()
{
applySharpen(m_video->sharpenAmount() + VideoWidget::kSharpenStep);
}
void MainWindow::decreaseSharpen()
{
applySharpen(m_video->sharpenAmount() - VideoWidget::kSharpenStep);
}
void MainWindow::applySharpen(float value)
{
value = qBound(VideoWidget::kSharpenMin, value, VideoWidget::kSharpenMax);
m_video->setSharpenAmount(value);
if (m_sharpenAction) m_sharpenAction->setChecked(value > 0.0f);
QSettings settings;
settings.setValue("display/sharpen", static_cast<double>(value));
if (value > 0.0f) {
m_lastSharpenAmount = value;
settings.setValue("display/sharpen_last", static_cast<double>(value));
}
statusBar()->showMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1), 2000);
showOverlayMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1));
}
void MainWindow::increaseBrightness()
{
applyBrightness(m_video->brightness() + VideoWidget::kBrightnessStep);
}
void MainWindow::decreaseBrightness()
{
applyBrightness(m_video->brightness() - VideoWidget::kBrightnessStep);
}
void MainWindow::toggleRaw()
{
const bool enabled = !m_video->rawEnabled();
m_video->setRawEnabled(enabled);
const QString msg = enabled ? "Roh-Modus: an (alle Effekte umgangen)" : "Roh-Modus: aus";
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::toggleCompare()
{
const bool enabled = !m_video->compareEnabled();
m_video->setCompareEnabled(enabled);
const QString msg = enabled ? "Vergleich: an (links roh, rechts bearbeitet)" : "Vergleich: aus";
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::saveScreenshot()
{
const QImage img = m_video->grabProcessedFrame();
if (img.isNull()) {
statusBar()->showMessage("Screenshot fehlgeschlagen (kein Bild)", 3000);
return;
}
const QString path = QDir::homePath() + "/hdmiviewer_screenshot_" +
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".png";
if (img.save(path, "PNG")) {
statusBar()->showMessage("Screenshot gespeichert: " + path, 5000);
showOverlayMessage("Screenshot gespeichert");
} else {
statusBar()->showMessage("Screenshot konnte nicht gespeichert werden: " + path, 5000);
}
}
void MainWindow::applyBrightness(float value)
{
value = qBound(VideoWidget::kBrightnessMin, value, VideoWidget::kBrightnessMax);
m_video->setBrightness(value);
QSettings settings;
settings.setValue("display/brightness", static_cast<double>(value));
statusBar()->showMessage(
QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)), 2000);
showOverlayMessage(QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)));
}
void MainWindow::showAbout() void MainWindow::showAbout()
{ {
auto* dlg = new QDialog(this); auto* dlg = new QDialog(this);
dlg->setWindowTitle("Über HDMI Viewer"); dlg->setWindowTitle("Über HDMI Viewer");
dlg->setFixedSize(420, 340); dlg->setFixedSize(460, 470);
dlg->setModal(true); dlg->setModal(true);
auto* root = new QVBoxLayout(dlg); auto* root = new QVBoxLayout(dlg);
@@ -125,26 +345,37 @@ void MainWindow::showAbout()
grid->addWidget(v, row, 1); grid->addWidget(v, row, 1);
}; };
addRow(0, "Version", "1.3.0"); addRow(0, "Version", "1.4.0");
addRow(1, "Entwickler", "Dany Thinnes"); addRow(1, "Entwickler", "Dany Thinnes");
addRow(2, "Projekt", "Projekt Hirnfrei"); addRow(2, "Projekt", "Projekt Hirnfrei");
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR)); addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
addRow(4, "Sprache", "C++17"); addRow(4, "Sprache", "C++17");
addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12"); addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12");
addRow(6, "Audio-API", "ALSA"); addRow(6, "Audio-API", "ALSA");
addRow(7, "Rendering", "OpenGL 3.3 · Bildglättung / Retro-Optik");
// ── Tastenkürzel-Hinweis ────────────────────────────────────────────────── // ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
auto* sep2 = new QFrame(body); auto* sep2 = new QFrame(body);
sep2->setFrameShape(QFrame::HLine); sep2->setFrameShape(QFrame::HLine);
sep2->setStyleSheet("color: #313244;"); sep2->setStyleSheet("color: #313244;");
grid->addWidget(sep2, 7, 0, 1, 2); grid->addWidget(sep2, 8, 0, 1, 2);
auto* keysLabel = new QLabel( auto* keysLabel = new QLabel(
"<small><b>F</b> &nbsp;Vollbild ein/aus &nbsp;&nbsp; " "<small><b>F</b> Vollbild &nbsp;&nbsp; "
"<b>Esc</b> &nbsp;Zurück zu Einstellungen</small>", body); "<b>S</b> Glättung &nbsp;&nbsp; "
"<b>C</b> Retro &nbsp;&nbsp; "
"<b>X</b>/<b>+</b>/<b>-</b> Schärfen &nbsp;&nbsp; "
"<b>,</b>/<b>.</b> Helligkeit<br>"
"<b>1</b>/<b>2</b> HW-Kontrast &nbsp;&nbsp; "
"<b>3</b>/<b>4</b> HW-Sättigung &nbsp;&nbsp; "
"<b>R</b> Roh-Diagnose<br>"
"<b>V</b> Vergleich &nbsp;&nbsp; "
"<b>P</b> Screenshot &nbsp;&nbsp; "
"<b>D</b> Frame dumpen &nbsp;&nbsp; "
"<b>Esc</b> Zurück</small>", body);
keysLabel->setStyleSheet("color: #6c7086;"); keysLabel->setStyleSheet("color: #6c7086;");
keysLabel->setAlignment(Qt::AlignCenter); keysLabel->setAlignment(Qt::AlignCenter);
grid->addWidget(keysLabel, 8, 0, 1, 2); grid->addWidget(keysLabel, 9, 0, 1, 2);
// ── Schließen-Button ────────────────────────────────────────────────────── // ── Schließen-Button ──────────────────────────────────────────────────────
auto* btnRow = new QHBoxLayout; auto* btnRow = new QHBoxLayout;
@@ -194,6 +425,52 @@ void MainWindow::keyPressEvent(QKeyEvent* event)
close(); close();
} }
break; break;
case Qt::Key_S:
toggleSmoothMode();
break;
case Qt::Key_C:
toggleRetroMode();
break;
case Qt::Key_X:
toggleSharpen();
break;
case Qt::Key_Plus:
case Qt::Key_Equal: // '+' liegt auf vielen Layouts ohne Shift auf dieser Taste
increaseSharpen();
break;
case Qt::Key_Minus:
decreaseSharpen();
break;
case Qt::Key_Period:
increaseBrightness();
break;
case Qt::Key_Comma:
decreaseBrightness();
break;
case Qt::Key_R:
toggleRaw();
break;
case Qt::Key_D:
if (m_capture) m_capture->requestFrameDump();
break;
case Qt::Key_V:
toggleCompare();
break;
case Qt::Key_P:
saveScreenshot();
break;
case Qt::Key_1:
if (m_capture) m_capture->adjustHardwareContrast(-5);
break;
case Qt::Key_2:
if (m_capture) m_capture->adjustHardwareContrast(5);
break;
case Qt::Key_3:
if (m_capture) m_capture->adjustHardwareSaturation(-5);
break;
case Qt::Key_4:
if (m_capture) m_capture->adjustHardwareSaturation(5);
break;
default: default:
QMainWindow::keyPressEvent(event); QMainWindow::keyPressEvent(event);
} }
@@ -226,7 +503,7 @@ void MainWindow::updateStatusBar()
info = QString("%1×%2 @ %3fps %4 ") info = QString("%1×%2 @ %3fps %4 ")
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt); .arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
info += QString("Video: %1 Audio: %2 [F] Vollbild [Esc] Einstellungen") info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
.arg(m_videoDevice, .arg(m_videoDevice,
m_audioDevice.isEmpty() ? QStringLiteral("") : m_audioDevice); m_audioDevice.isEmpty() ? QStringLiteral("") : m_audioDevice);
@@ -244,6 +521,20 @@ void MainWindow::onCaptureInfo(int width, int height, int fps, const QString& fo
.arg(width).arg(height).arg(fps).arg(format)); .arg(width).arg(height).arg(fps).arg(format));
} }
void MainWindow::onFrameDumped(const QString& path)
{
const QString msg = "Frame gespeichert: " + path;
statusBar()->showMessage(msg, 5000);
showOverlayMessage("Frame gespeichert (siehe Home-Ordner)");
}
void MainWindow::onHardwareControlChanged(const QString& label, int value, int minVal, int maxVal)
{
const QString msg = QString("%1: %2 (%3..%4)").arg(label).arg(value).arg(minVal).arg(maxVal);
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::onError(const QString& msg) void MainWindow::onError(const QString& msg)
{ {
statusBar()->showMessage("" + msg, 8000); statusBar()->showMessage("" + msg, 8000);

View File

@@ -6,6 +6,10 @@
class VideoWidget; class VideoWidget;
class CaptureThread; class CaptureThread;
class AudioThread; class AudioThread;
QT_BEGIN_NAMESPACE
class QAction;
class QLabel;
QT_END_NAMESPACE
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@@ -15,6 +19,11 @@ public:
MainWindow(const QString& videoDevice, MainWindow(const QString& videoDevice,
const QString& audioDevice, const QString& audioDevice,
int targetFps, int targetFps,
bool smoothModeEnabled = true,
bool retroModeEnabled = false,
float brightness = 1.0f,
float sharpenAmount = 0.0f,
float lastSharpenAmount = 2.5f,
QWidget* parent = nullptr); QWidget* parent = nullptr);
~MainWindow() override; ~MainWindow() override;
@@ -27,17 +36,37 @@ protected:
private slots: private slots:
void onError(const QString& msg); void onError(const QString& msg);
void onCaptureInfo(int width, int height, int fps, const QString& format); void onCaptureInfo(int width, int height, int fps, const QString& format);
void onFrameDumped(const QString& path);
void onHardwareControlChanged(const QString& label, int value, int minVal, int maxVal);
void showAbout(); void showAbout();
void toggleSmoothMode();
void toggleRetroMode();
void toggleSharpen();
void increaseSharpen();
void decreaseSharpen();
void increaseBrightness();
void decreaseBrightness();
void toggleRaw();
void toggleCompare();
void saveScreenshot();
private: private:
void setupMenuBar(); void setupMenuBar();
void toggleFullscreen(); void toggleFullscreen();
void stopAll(); void stopAll();
void updateStatusBar(); void updateStatusBar();
void applyBrightness(float value);
void applySharpen(float value);
void showOverlayMessage(const QString& text);
VideoWidget* m_video = nullptr; VideoWidget* m_video = nullptr;
CaptureThread* m_capture = nullptr; CaptureThread* m_capture = nullptr;
AudioThread* m_audio = nullptr; AudioThread* m_audio = nullptr;
QAction* m_smoothAction = nullptr;
QAction* m_retroAction = nullptr;
QAction* m_sharpenAction = nullptr;
QLabel* m_overlayLabel = nullptr;
float m_lastSharpenAmount = 2.5f; // für Taste X: Wert vorm letzten Ausschalten
QString m_videoDevice; QString m_videoDevice;
QString m_audioDevice; QString m_audioDevice;

View File

@@ -1,45 +1,450 @@
#include "VideoWidget.h" #include "VideoWidget.h"
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QPainter> #include <QPainter>
#include <QPaintEvent> #include <QVector2D>
namespace {
// Unit-Quad in NDC, per Vertex-Shader auf die gewünschte Letterbox-Größe
// skaliert. Texturkoordinaten so gewählt, dass QImage-Zeile 0 (oben) auf
// v=0 (oben im Bild) landet.
const float kQuadVerts[] = {
// x, y, u, v
-1.0f, 1.0f, 0.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 1.0f, 1.0f,
};
const char* kVertexShaderSrc = R"GLSL(
#version 330 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_tex;
uniform vec2 u_scale;
uniform vec2 u_offset;
out vec2 v_tex;
void main()
{
vec2 pos = a_pos * u_scale + u_offset;
gl_Position = vec4(pos, 0.0, 1.0);
v_tex = a_tex;
}
)GLSL";
// Zwei unabhängige, kombinierbare Effekte:
//
// 1) Bildglättung (u_smooth) die eigentliche Qualitätsverbesserung: trennt
// Luma/Chroma (wie ein Composite-Signal es tut) und behandelt sie
// unterschiedlich. Chroma wird stärker weichgezeichnet, weil billige
// Composite→HDMI-Chips dort Dot-Crawl/Farbrauschen produzieren das Auge
// ist für Farbdetails ohnehin unempfindlicher. Luma wird nur leicht
// geglättet (nimmt harte, blockige Scaler-Kanten raus) und danach dezent
// gegengeschärft, damit das Bild nicht matschig wirkt. Kein Nostalgie-
// Effekt, sondern Rekonstruktion in Richtung dessen, was ein Röhrenbild
// ohnehin automatisch getan hätte.
//
// 2) Retro-Optik (u_retro) der Nostalgie-Look von vorhin: Scanlines,
// Aperture-Grille-Maske, leichte Wölbung, Vignette. Rein optisch, wird
// nach der Glättung obendrauf gelegt, falls gewünscht.
const char* kFragmentShaderSrc = R"GLSL(
#version 330 core
in vec2 v_tex;
out vec4 fragColor;
uniform sampler2D u_tex;
uniform vec2 u_texSize;
uniform int u_smooth;
uniform int u_retro;
uniform float u_sharpen;
uniform float u_gamma;
uniform int u_raw;
uniform int u_compare;
vec3 rgb2ycbcr(vec3 c)
{
float y = dot(c, vec3(0.299, 0.587, 0.114));
float cb = dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5;
float cr = dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5;
return vec3(y, cb, cr);
}
vec3 ycbcr2rgb(vec3 c)
{
float y = c.x;
float cb = c.y - 0.5;
float cr = c.z - 0.5;
return vec3(
y + 1.402 * cr,
y - 0.344136 * cb - 0.714136 * cr,
y + 1.772 * cb
);
}
// Rekonstruiert Luma und Chroma getrennt mit einem echten, breiten 2D-
// Tiefpass (Radius 4 Texel statt vorher 1-2). Bei 1920×1080 ist ein
// 1-2-Texel-Radius schlicht zu schmal, um überhaupt wahrnehmbar zu sein
// und JPEG-Blockartefakte sitzen im 8×8-Pixel-Raster, das ein so schmaler
// Filter gar nicht berührt. Radius 4 ist die minimal sinnvolle Größe.
vec3 reconstruct(vec2 uv)
{
vec2 tx = vec2(1.0 / max(u_texSize.x, 1.0), 0.0);
vec2 ty = vec2(0.0, 1.0 / max(u_texSize.y, 1.0));
vec3 center = rgb2ycbcr(texture(u_tex, uv).rgb);
// Symmetrischer 9-Tap-Kern (Radius 4), auf 1.0 normiert
float w0 = 0.20, w1 = 0.16, w2 = 0.12, w3 = 0.08, w4 = 0.04;
vec3 blurH = center * w0;
blurH += rgb2ycbcr(texture(u_tex, uv - 1.0*tx).rgb) * w1;
blurH += rgb2ycbcr(texture(u_tex, uv + 1.0*tx).rgb) * w1;
blurH += rgb2ycbcr(texture(u_tex, uv - 2.0*tx).rgb) * w2;
blurH += rgb2ycbcr(texture(u_tex, uv + 2.0*tx).rgb) * w2;
blurH += rgb2ycbcr(texture(u_tex, uv - 3.0*tx).rgb) * w3;
blurH += rgb2ycbcr(texture(u_tex, uv + 3.0*tx).rgb) * w3;
blurH += rgb2ycbcr(texture(u_tex, uv - 4.0*tx).rgb) * w4;
blurH += rgb2ycbcr(texture(u_tex, uv + 4.0*tx).rgb) * w4;
vec3 blurV = center * w0;
blurV += rgb2ycbcr(texture(u_tex, uv - 1.0*ty).rgb) * w1;
blurV += rgb2ycbcr(texture(u_tex, uv + 1.0*ty).rgb) * w1;
blurV += rgb2ycbcr(texture(u_tex, uv - 2.0*ty).rgb) * w2;
blurV += rgb2ycbcr(texture(u_tex, uv + 2.0*ty).rgb) * w2;
blurV += rgb2ycbcr(texture(u_tex, uv - 3.0*ty).rgb) * w3;
blurV += rgb2ycbcr(texture(u_tex, uv + 3.0*ty).rgb) * w3;
blurV += rgb2ycbcr(texture(u_tex, uv - 4.0*ty).rgb) * w4;
blurV += rgb2ycbcr(texture(u_tex, uv + 4.0*ty).rgb) * w4;
vec3 blur2D = (blurH + blurV) * 0.5;
// Adaptive Stärke: dunkle Bereiche stärker (Kompressionsrauschen), aber
// die Basis liegt jetzt nahe 100% reinem Blur KEIN interner
// Gegenschärfen-Anteil mehr (der hat vorher einen Großteil des Effekts
// wieder zurückgenommen, ohne dass es nach außen sichtbar war). Schärfen
// ist jetzt ausschließlich Aufgabe von sharpen()/Taste X.
float darkFactor = clamp(1.0 - center.x / 0.3, 0.0, 1.0);
float lumaMixAmt = mix(0.92, 0.99, darkFactor);
float chromaMixAmt = mix(0.95, 0.99, darkFactor);
float lumaOut = mix(center.x, blur2D.x, lumaMixAmt);
vec2 chromaOut = mix(center.yz, blur2D.yz, chromaMixAmt);
return ycbcr2rgb(vec3(lumaOut, chromaOut));
}
// Nachschärfen (Unsharp Mask) übers gesamte Bild, unabhängig von Glättung/
// Retro-Optik. Stärke kommt jetzt vollständig aus u_sharpen (per Taste +/-
// live einstellbar), damit sich die Wirkschwelle experimentell finden lässt.
vec3 sharpen(vec2 uv, vec3 baseColor, float amount)
{
vec2 tx = vec2(1.0 / max(u_texSize.x, 1.0), 0.0);
vec2 ty = vec2(0.0, 1.0 / max(u_texSize.y, 1.0));
vec3 raw = texture(u_tex, uv).rgb;
vec3 blur = raw * 0.20;
blur += (texture(u_tex, uv - tx).rgb + texture(u_tex, uv + tx).rgb
+ texture(u_tex, uv - ty).rgb + texture(u_tex, uv + ty).rgb) * 0.10;
blur += (texture(u_tex, uv - 2.0*tx).rgb + texture(u_tex, uv + 2.0*tx).rgb
+ texture(u_tex, uv - 2.0*ty).rgb + texture(u_tex, uv + 2.0*ty).rgb) * 0.10;
return baseColor + amount * (raw - blur);
}
void main()
{
vec2 uv = v_tex;
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
if (u_raw == 1) {
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
return;
}
float r2 = 0.0;
if (u_retro == 1) {
vec2 c = uv * 2.0 - 1.0;
r2 = dot(c, c);
vec2 warped = c * (1.0 + 0.035 * r2);
uv = warped * 0.5 + 0.5;
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
}
vec3 color = (u_smooth == 1) ? reconstruct(uv) : texture(u_tex, uv).rgb;
if (u_retro == 1) {
float lines = 240.0;
float linePhase = fract(uv.y * lines) * 2.0 - 1.0;
float scan = 1.0 - 0.28 * (1.0 - linePhase * linePhase);
color *= scan;
float maskX = mod(gl_FragCoord.x, 3.0);
vec3 mask = vec3(0.82);
if (maskX < 1.0) mask.r = 1.08;
else if (maskX < 2.0) mask.g = 1.08;
else mask.b = 1.08;
color *= mask;
float vig = clamp(1.0 - 0.22 * r2, 0.0, 1.0);
color *= vig;
color *= 1.18;
}
// Signalbereich-Korrektur: viele billige Grabber/Scaler liefern effektiv
// nur Studio-/Broadcast-Range statt vollem 0255-Kontrastumfang
// unabhängig vom Pixelformat, betrifft also auch MJPEG (wo eine reine
// YUV-seitige Korrektur nicht greifen würde). Schwarzpunkt bewusst nahe
// 0, damit möglichst viel Schattendetail erhalten bleibt das adaptive
// Rauschunterdrücken oben in reconstruct() fängt das dabei entstehende
// Rauschen in genau diesen dunklen Bereichen gezielt ab.
const float kBlack = 1.0 / 255.0;
const float kWhite = 235.0 / 255.0;
color = clamp((color - kBlack) / (kWhite - kBlack), 0.0, 1.0);
if (u_sharpen > 0.0) {
color = sharpen(uv, color, u_sharpen);
}
// Helligkeit: Gamma-Anpassung NUR auf Luma (nicht pro RGB-Kanal einzeln!)
// sonst verschiebt sich bei verrauschten/dunklen Pixeln der Farbton,
// und Kompressionsrauschen wird bunt statt nur heller.
vec3 ycc = rgb2ycbcr(color);
ycc.x = pow(max(ycc.x, 0.0), 1.0 / u_gamma);
// Kontrast-Punch: leichte S-Kurve um die Mitte, deutlich zurückhaltender
// als vorher (war 1.12) sattere Schwarz-/Hellwerte, ohne bei dunklen
// Szenen spürbar Helligkeit zu kosten.
ycc.x = clamp((ycc.x - 0.5) * 1.05 + 0.5, 0.0, 1.0);
float targetY = ycc.x;
// Sättigung: etwas kräftiger als roh, aber ebenfalls zurückhaltender
// (war 1.35). Wichtiger: nach der Sättigungsanhebung wird die
// tatsächliche Helligkeit der resultierenden Farbe gemessen und wieder
// auf targetY zurückskaliert. Grund: Die YCbCr→RGB-Formel zieht den
// Grün-Kanal bei der Rückrechnung von Cb/Cr ab bei warmen/orangenen
// Szenen (starkes Cr) senkt eine Sättigungsanhebung dadurch spürbar die
// wahrgenommene Helligkeit, obwohl Y unverändert blieb. Diese
// Renormierung verhindert genau dieses szenenabhängige Abdunkeln.
ycc.y = clamp((ycc.y - 0.5) * 1.15 + 0.5, 0.0, 1.0);
ycc.z = clamp((ycc.z - 0.5) * 1.15 + 0.5, 0.0, 1.0);
color = ycbcr2rgb(ycc);
float actualY = dot(color, vec3(0.299, 0.587, 0.114));
if (actualY > 0.001) {
color *= targetY / actualY;
}
// Live-Vergleich: linke Bildhälfte roh, rechte Hälfte komplett bearbeitet
// (mit allen aktuell aktiven Reglern) zum direkten Abgleich beim
// Fein-Tunen. Nutzt bewusst v_tex (unverzerrte Original-Koordinate),
// nicht das ggf. durch Retro-Optik gewölbte uv, damit die rohe Seite
// wirklich unverändert bleibt.
if (u_compare == 1) {
vec3 rawColor = texture(u_tex, v_tex).rgb;
if (v_tex.x < 0.5) {
color = rawColor;
}
if (abs(v_tex.x - 0.5) < 0.0015) {
color = vec3(1.0, 0.9, 0.1); // gelbe Trennlinie
}
}
fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
}
)GLSL";
} // namespace
VideoWidget::VideoWidget(QWidget* parent) VideoWidget::VideoWidget(QWidget* parent)
: QWidget(parent) : QOpenGLWidget(parent)
{ {
setMinimumSize(640, 360); setMinimumSize(640, 360);
// Schwarzer Hintergrund }
QPalette pal = palette();
pal.setColor(QPalette::Window, Qt::black); VideoWidget::~VideoWidget()
setPalette(pal); {
setAutoFillBackground(true); makeCurrent();
delete m_texture;
m_texture = nullptr;
if (m_vbo) { glDeleteBuffers(1, &m_vbo); m_vbo = 0; }
if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; }
delete m_program;
m_program = nullptr;
doneCurrent();
} }
void VideoWidget::setFrame(const QImage& image) void VideoWidget::setFrame(const QImage& image)
{ {
if (image.isNull()) return; if (image.isNull()) return;
m_pixmap = QPixmap::fromImage(image); m_pendingImage = image;
m_pendingDirty = true;
update(); update();
} }
void VideoWidget::paintEvent(QPaintEvent* /*event*/) void VideoWidget::setSmoothModeEnabled(bool enabled)
{
if (m_smoothEnabled == enabled) return;
m_smoothEnabled = enabled;
update();
}
void VideoWidget::setRetroModeEnabled(bool enabled)
{
if (m_retroEnabled == enabled) return;
m_retroEnabled = enabled;
update();
}
void VideoWidget::setSharpenAmount(float amount)
{
amount = qBound(kSharpenMin, amount, kSharpenMax);
if (qFuzzyCompare(m_sharpenAmount, amount)) return;
m_sharpenAmount = amount;
update();
}
void VideoWidget::setRawEnabled(bool enabled)
{
if (m_rawEnabled == enabled) return;
m_rawEnabled = enabled;
update();
}
void VideoWidget::setCompareEnabled(bool enabled)
{
if (m_compareEnabled == enabled) return;
m_compareEnabled = enabled;
update();
}
void VideoWidget::setBrightness(float gamma)
{
gamma = qBound(kBrightnessMin, gamma, kBrightnessMax);
if (qFuzzyCompare(m_brightness, gamma)) return;
m_brightness = gamma;
update();
}
void VideoWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
m_program = new QOpenGLShaderProgram(this);
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
m_program->link();
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVerts), kQuadVerts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(0));
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
glBindVertexArray(0);
}
void VideoWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
void VideoWidget::uploadPendingFrame()
{
if (!m_pendingDirty || m_pendingImage.isNull())
return;
const QImage rgba = m_pendingImage.convertToFormat(QImage::Format_RGBA8888);
if (!m_texture || m_texture->width() != rgba.width() || m_texture->height() != rgba.height()) {
delete m_texture;
m_texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
m_texture->create();
m_texture->setFormat(QOpenGLTexture::RGBA8_UNorm);
m_texture->setSize(rgba.width(), rgba.height());
m_texture->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8);
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
}
m_texture->setData(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, rgba.constBits());
m_imageSize = rgba.size();
m_hasFrame = true;
m_pendingDirty = false;
}
void VideoWidget::drawNoSignal()
{ {
QPainter p(this); QPainter p(this);
p.setRenderHint(QPainter::SmoothPixmapTransform);
if (m_pixmap.isNull()) {
p.fillRect(rect(), Qt::black);
p.setPen(Qt::darkGray); p.setPen(Qt::darkGray);
p.drawText(rect(), Qt::AlignCenter, "Kein Signal"); p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
}
void VideoWidget::paintGL()
{
uploadPendingFrame();
glClear(GL_COLOR_BUFFER_BIT);
if (!m_hasFrame || !m_texture) {
drawNoSignal();
return; return;
} }
// Seitenverhältnis bewahren (letterbox/pillarbox) const float viewW = static_cast<float>(width());
QRect target = rect(); const float viewH = static_cast<float>(height());
QSize scaled = m_pixmap.size().scaled(target.size(), Qt::KeepAspectRatio); const float imgW = static_cast<float>(m_imageSize.width());
QPoint topLeft( const float imgH = static_cast<float>(m_imageSize.height());
target.x() + (target.width() - scaled.width()) / 2,
target.y() + (target.height() - scaled.height()) / 2 float scaleX = 1.0f;
); float scaleY = 1.0f;
p.fillRect(rect(), Qt::black); if (imgW > 0.0f && imgH > 0.0f && viewW > 0.0f && viewH > 0.0f) {
p.drawPixmap(QRect(topLeft, scaled), m_pixmap, m_pixmap.rect()); const float imageAspect = imgW / imgH;
const float viewAspect = viewW / viewH;
if (imageAspect > viewAspect) {
scaleX = 1.0f;
scaleY = viewAspect / imageAspect;
} else {
scaleY = 1.0f;
scaleX = imageAspect / viewAspect;
}
}
m_program->bind();
glBindVertexArray(m_vao);
m_texture->bind(0);
m_program->setUniformValue("u_scale", QVector2D(scaleX, scaleY));
m_program->setUniformValue("u_offset", QVector2D(0.0f, 0.0f));
m_program->setUniformValue("u_tex", 0);
m_program->setUniformValue("u_texSize", QVector2D(imgW, imgH));
m_program->setUniformValue("u_smooth", m_smoothEnabled ? 1 : 0);
m_program->setUniformValue("u_retro", m_retroEnabled ? 1 : 0);
m_program->setUniformValue("u_sharpen", m_sharpenAmount);
m_program->setUniformValue("u_gamma", m_brightness);
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
m_texture->release();
glBindVertexArray(0);
m_program->release();
} }

View File

@@ -1,22 +1,79 @@
#pragma once #pragma once
#include <QWidget> #include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QImage> #include <QImage>
#include <QPixmap> #include <QSize>
class VideoWidget : public QWidget QT_BEGIN_NAMESPACE
class QOpenGLShaderProgram;
class QOpenGLTexture;
QT_END_NAMESPACE
// VideoWidget zeichnet das eingehende Frame per OpenGL (statt QPainter/CPU).
// Zwei unabhängige Effekte können live per Shader zugeschaltet werden:
// Bildglättung (Chroma/Luma-Rekonstruktion gegen Scaler-Artefakte, echte
// Bildverbesserung) und Retro-Optik (Scanlines/Maske/Wölbung, Nostalgie-Look).
// QOpenGLExtraFunctions (statt nur QOpenGLFunctions) wird benötigt, weil
// Vertex-Array-Objects (glGenVertexArrays & Co.) nicht Teil des reinen
// ES2-kompatiblen QOpenGLFunctions-Subsets sind.
class VideoWidget : public QOpenGLWidget, protected QOpenGLExtraFunctions
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit VideoWidget(QWidget* parent = nullptr); explicit VideoWidget(QWidget* parent = nullptr);
~VideoWidget() override;
public slots: public slots:
void setFrame(const QImage& image); void setFrame(const QImage& image);
void setSmoothModeEnabled(bool enabled);
void setRetroModeEnabled(bool enabled);
void setSharpenAmount(float amount);
void setBrightness(float gamma);
void setRawEnabled(bool enabled);
void setCompareEnabled(bool enabled);
bool smoothModeEnabled() const { return m_smoothEnabled; }
bool retroModeEnabled() const { return m_retroEnabled; }
float sharpenAmount() const { return m_sharpenAmount; }
float brightness() const { return m_brightness; }
bool rawEnabled() const { return m_rawEnabled; }
bool compareEnabled() const { return m_compareEnabled; }
QImage grabProcessedFrame() { return grabFramebuffer(); }
protected: protected:
void paintEvent(QPaintEvent* event) override; void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
private: private:
QPixmap m_pixmap; void uploadPendingFrame();
void drawNoSignal();
QOpenGLShaderProgram* m_program = nullptr;
QOpenGLTexture* m_texture = nullptr;
unsigned int m_vbo = 0;
unsigned int m_vao = 0;
QImage m_pendingImage; // Wartet auf Upload im GL-Thread (paintGL)
bool m_pendingDirty = false;
QSize m_imageSize; // Auflösung des zuletzt hochgeladenen Frames
bool m_hasFrame = false;
bool m_smoothEnabled = true; // Bildglättung: Chroma/Luma-Rekonstruktion gegen Scaler-Artefakte
bool m_retroEnabled = false; // Retro-Optik: Scanlines/Maske/Wölbung (Nostalgie-Look)
float m_sharpenAmount = 0.0f; // Nachschärfen (Unsharp Mask): 0 = aus
float m_brightness = 1.0f; // Gamma-Faktor: 1.0 = neutral, >1 heller, <1 dunkler
bool m_rawEnabled = false; // Diagnose: umgeht ALLES (Studio-Range, Kontrast, Sättigung, Gamma)
bool m_compareEnabled = false; // Live-Vergleich: links roh, rechts bearbeitet
public:
static constexpr float kBrightnessMin = 0.4f;
static constexpr float kBrightnessMax = 3.2f;
static constexpr float kBrightnessStep = 0.1f;
static constexpr float kSharpenMin = 0.0f;
static constexpr float kSharpenMax = 20.0f;
static constexpr float kSharpenStep = 0.5f;
static constexpr float kSharpenDefault = 2.5f; // Wert für Taste X (Quick-Toggle)
}; };

View File

@@ -1,15 +1,24 @@
#include <QApplication> #include <QApplication>
#include <QSettings> #include <QSettings>
#include <QMessageBox> #include <QMessageBox>
#include <QSurfaceFormat>
#include "DeviceDialog.h" #include "DeviceDialog.h"
#include "MainWindow.h" #include "MainWindow.h"
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
// OpenGL 3.3 Core für die Bildglättungs-/Retro-Optik-Shader in VideoWidget.
// Muss vor der QApplication gesetzt werden.
QSurfaceFormat fmt;
fmt.setVersion(3, 3);
fmt.setProfile(QSurfaceFormat::CoreProfile);
fmt.setSwapInterval(1);
QSurfaceFormat::setDefaultFormat(fmt);
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("HDMIViewer"); app.setApplicationName("HDMIViewer");
app.setApplicationVersion("1.2"); app.setApplicationVersion("1.4.0");
app.setOrganizationName("hdmi-viewer"); app.setOrganizationName("hdmi-viewer");
QSettings settings; QSettings settings;
@@ -19,6 +28,12 @@ int main(int argc, char* argv[])
dlg.setPreselectedVideo(settings.value("device/video").toString()); dlg.setPreselectedVideo(settings.value("device/video").toString());
dlg.setPreselectedAudio(settings.value("device/audio", "default").toString()); dlg.setPreselectedAudio(settings.value("device/audio", "default").toString());
dlg.setPreselectedFps(settings.value("device/fps", 30).toInt()); dlg.setPreselectedFps(settings.value("device/fps", 30).toInt());
dlg.setPreselectedSmoothMode(settings.value("display/smooth", true).toBool());
dlg.setPreselectedRetroMode(settings.value("display/retro", false).toBool());
dlg.setPreselectedSharpenAmount(settings.value("display/sharpen", 0.0).toFloat());
dlg.setPreselectedBrightness(settings.value("display/brightness", 1.0).toFloat());
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
if (dlg.exec() != QDialog::Accepted) if (dlg.exec() != QDialog::Accepted)
break; break;
@@ -26,6 +41,18 @@ int main(int argc, char* argv[])
const QString videoDevice = dlg.selectedVideoDevice(); const QString videoDevice = dlg.selectedVideoDevice();
const QString audioDevice = dlg.selectedAudioDevice(); const QString audioDevice = dlg.selectedAudioDevice();
const int fps = dlg.selectedFps(); const int fps = dlg.selectedFps();
const bool smoothMode = dlg.selectedSmoothMode();
const bool retroMode = dlg.selectedRetroMode();
const float sharpenAmt = dlg.selectedSharpenAmount();
const float brightness = dlg.selectedBrightness();
const int hwContrast = dlg.selectedHwContrast();
const int hwSaturation = dlg.selectedHwSaturation();
// Für die Taste [X] (Quick-Toggle 0 ↔ letzter Wert) zur Laufzeit:
// Falls im Dialog ein Wert >0 gesetzt wurde, ist das gleichzeitig
// der zu merkende "letzte" Wert.
const float lastSharpen = sharpenAmt > 0.0f
? sharpenAmt
: settings.value("display/sharpen_last", 2.5).toFloat();
if (videoDevice.isEmpty()) { if (videoDevice.isEmpty()) {
QMessageBox::critical(nullptr, "Fehler", QMessageBox::critical(nullptr, "Fehler",
@@ -36,9 +63,25 @@ int main(int argc, char* argv[])
settings.setValue("device/video", videoDevice); settings.setValue("device/video", videoDevice);
settings.setValue("device/audio", audioDevice); settings.setValue("device/audio", audioDevice);
settings.setValue("device/fps", fps); settings.setValue("device/fps", fps);
settings.setValue("display/smooth", smoothMode);
settings.setValue("display/retro", retroMode);
settings.setValue("display/sharpen", static_cast<double>(sharpenAmt));
settings.setValue("display/brightness", static_cast<double>(brightness));
if (sharpenAmt > 0.0f)
settings.setValue("display/sharpen_last", static_cast<double>(sharpenAmt));
// -1 = "unverändert" -> kein expliziter Nutzerwert, Regler bleibt
// beim Öffnen unangetastet (siehe CaptureThread::applyHardwareControls)
if (hwContrast >= 0)
settings.setValue("hardware/contrast", hwContrast);
else
settings.remove("hardware/contrast");
if (hwSaturation >= 0)
settings.setValue("hardware/saturation", hwSaturation);
else
settings.remove("hardware/saturation");
settings.sync(); settings.sync();
MainWindow win(videoDevice, audioDevice, fps); MainWindow win(videoDevice, audioDevice, fps, smoothMode, retroMode, brightness, sharpenAmt, lastSharpen);
win.show(); win.show();
app.exec(); app.exec();