Erster Teststream absolviert. Positives Ergebnis
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
#include "CaptureThread.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QSettings>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
@@ -16,6 +20,48 @@ static int xioctl(int fd, unsigned long request, void* arg)
|
||||
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) ──────────────────────────────────────
|
||||
|
||||
static inline quint8 clamp255(int v)
|
||||
@@ -40,6 +86,11 @@ void CaptureThread::stopAndWait()
|
||||
wait();
|
||||
}
|
||||
|
||||
void CaptureThread::requestFrameDump()
|
||||
{
|
||||
m_dumpRequested = true;
|
||||
}
|
||||
|
||||
void CaptureThread::run()
|
||||
{
|
||||
if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) {
|
||||
@@ -117,6 +168,10 @@ bool CaptureThread::openDevice()
|
||||
emit errorOccurred(m_device + " unterstützt kein Streaming");
|
||||
return false;
|
||||
}
|
||||
|
||||
logAvailableControls(m_fd);
|
||||
applyHardwareControls();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -273,14 +328,131 @@ void CaptureThread::closeDevice()
|
||||
m_buffers.clear();
|
||||
|
||||
if (m_fd != -1) {
|
||||
restoreHardwareControls();
|
||||
::close(m_fd);
|
||||
m_fd = -1;
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
switch (m_pixelFormat) {
|
||||
case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break;
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
~CaptureThread() override;
|
||||
|
||||
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 captureHeight() const { return m_height; }
|
||||
@@ -32,6 +35,8 @@ signals:
|
||||
void frameReady(QImage image);
|
||||
void errorOccurred(const QString& message);
|
||||
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:
|
||||
void run() override;
|
||||
@@ -45,6 +50,9 @@ private:
|
||||
void closeDevice();
|
||||
bool initMmap();
|
||||
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
|
||||
QImage decodeYUYV(const void* data) const;
|
||||
@@ -60,4 +68,11 @@ private:
|
||||
quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG;
|
||||
std::vector<Buffer> m_buffers;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "DeviceDialog.h"
|
||||
#include "VideoWidget.h" // für kBrightnessMin/Max, kSharpenMin/Max (eine Quelle der Wahrheit)
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QSpinBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QDir>
|
||||
@@ -51,11 +55,79 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
||||
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
||||
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(
|
||||
"<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 "
|
||||
"Titelleiste angezeigt.<br>"
|
||||
"<b>F</b> = Vollbild · <b>Esc</b> = zurück hierher</small>",
|
||||
"<b>F</b> Vollbild · <b>S</b> Glättung · "
|
||||
"<b>C</b> Retro · <b>X/+/-</b> Schärfen · "
|
||||
"<b>,/.</b> Helligkeit · <b>1/2</b> HW-Kontrast · "
|
||||
"<b>3/4</b> HW-Sättigung · <b>V</b> Vergleich · "
|
||||
"<b>P</b> Screenshot · <b>Esc</b> zurück hierher</small>",
|
||||
this);
|
||||
hint->setWordWrap(true);
|
||||
|
||||
@@ -81,10 +153,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
||||
QString DeviceDialog::selectedVideoDevice() const { return m_videoCombo->currentData().toString(); }
|
||||
QString DeviceDialog::selectedAudioDevice() const { return m_audioCombo->currentData().toString(); }
|
||||
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::setPreselectedAudio(const QString& device) { selectByData(m_audioCombo, device); }
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QComboBox;
|
||||
class QCheckBox;
|
||||
class QDoubleSpinBox;
|
||||
class QSpinBox;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class DeviceDialog : public QDialog
|
||||
@@ -17,17 +20,35 @@ public:
|
||||
QString selectedVideoDevice() const;
|
||||
QString selectedAudioDevice() 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 setPreselectedAudio(const QString& device);
|
||||
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:
|
||||
void populateVideoDevices();
|
||||
void populateAudioDevices();
|
||||
void selectByData(QComboBox* combo, const QString& data);
|
||||
|
||||
QComboBox* m_videoCombo = nullptr;
|
||||
QComboBox* m_audioCombo = nullptr;
|
||||
QComboBox* m_fpsCombo = nullptr;
|
||||
QComboBox* m_videoCombo = nullptr;
|
||||
QComboBox* m_audioCombo = 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;
|
||||
};
|
||||
|
||||
@@ -9,18 +9,28 @@
|
||||
#include <QMenuBar>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QSettings>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QFrame>
|
||||
#include <QTimer>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
MainWindow::MainWindow(const QString& videoDevice,
|
||||
const QString& audioDevice,
|
||||
int targetFps,
|
||||
bool smoothModeEnabled,
|
||||
bool retroModeEnabled,
|
||||
float brightness,
|
||||
float sharpenAmount,
|
||||
float lastSharpenAmount,
|
||||
QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
, m_lastSharpenAmount(lastSharpenAmount)
|
||||
, m_videoDevice(videoDevice)
|
||||
, m_audioDevice(audioDevice)
|
||||
{
|
||||
@@ -28,8 +38,20 @@ MainWindow::MainWindow(const QString& videoDevice,
|
||||
resize(1280, 720);
|
||||
|
||||
m_video = new VideoWidget(this);
|
||||
m_video->setSmoothModeEnabled(smoothModeEnabled);
|
||||
m_video->setRetroModeEnabled(retroModeEnabled);
|
||||
m_video->setSharpenAmount(sharpenAmount);
|
||||
m_video->setBrightness(brightness);
|
||||
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();
|
||||
updateStatusBar();
|
||||
|
||||
@@ -45,6 +67,12 @@ MainWindow::MainWindow(const QString& videoDevice,
|
||||
connect(m_capture, &CaptureThread::captureInfo,
|
||||
this, &MainWindow::onCaptureInfo,
|
||||
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();
|
||||
|
||||
@@ -65,6 +93,54 @@ MainWindow::~MainWindow()
|
||||
|
||||
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");
|
||||
|
||||
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
||||
@@ -72,11 +148,155 @@ void MainWindow::setupMenuBar()
|
||||
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()
|
||||
{
|
||||
auto* dlg = new QDialog(this);
|
||||
dlg->setWindowTitle("Über HDMI Viewer");
|
||||
dlg->setFixedSize(420, 340);
|
||||
dlg->setFixedSize(460, 470);
|
||||
dlg->setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(dlg);
|
||||
@@ -125,26 +345,37 @@ void MainWindow::showAbout()
|
||||
grid->addWidget(v, row, 1);
|
||||
};
|
||||
|
||||
addRow(0, "Version", "1.3.0");
|
||||
addRow(0, "Version", "1.4.0");
|
||||
addRow(1, "Entwickler", "Dany Thinnes");
|
||||
addRow(2, "Projekt", "Projekt Hirnfrei");
|
||||
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
||||
addRow(4, "Sprache", "C++17");
|
||||
addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12");
|
||||
addRow(6, "Audio-API", "ALSA");
|
||||
addRow(7, "Rendering", "OpenGL 3.3 · Bildglättung / Retro-Optik");
|
||||
|
||||
// ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
|
||||
auto* sep2 = new QFrame(body);
|
||||
sep2->setFrameShape(QFrame::HLine);
|
||||
sep2->setStyleSheet("color: #313244;");
|
||||
grid->addWidget(sep2, 7, 0, 1, 2);
|
||||
grid->addWidget(sep2, 8, 0, 1, 2);
|
||||
|
||||
auto* keysLabel = new QLabel(
|
||||
"<small><b>F</b> Vollbild ein/aus "
|
||||
"<b>Esc</b> Zurück zu Einstellungen</small>", body);
|
||||
"<small><b>F</b> Vollbild "
|
||||
"<b>S</b> Glättung "
|
||||
"<b>C</b> Retro "
|
||||
"<b>X</b>/<b>+</b>/<b>-</b> Schärfen "
|
||||
"<b>,</b>/<b>.</b> Helligkeit<br>"
|
||||
"<b>1</b>/<b>2</b> HW-Kontrast "
|
||||
"<b>3</b>/<b>4</b> HW-Sättigung "
|
||||
"<b>R</b> Roh-Diagnose<br>"
|
||||
"<b>V</b> Vergleich "
|
||||
"<b>P</b> Screenshot "
|
||||
"<b>D</b> Frame dumpen "
|
||||
"<b>Esc</b> Zurück</small>", body);
|
||||
keysLabel->setStyleSheet("color: #6c7086;");
|
||||
keysLabel->setAlignment(Qt::AlignCenter);
|
||||
grid->addWidget(keysLabel, 8, 0, 1, 2);
|
||||
grid->addWidget(keysLabel, 9, 0, 1, 2);
|
||||
|
||||
// ── Schließen-Button ──────────────────────────────────────────────────────
|
||||
auto* btnRow = new QHBoxLayout;
|
||||
@@ -194,6 +425,52 @@ void MainWindow::keyPressEvent(QKeyEvent* event)
|
||||
close();
|
||||
}
|
||||
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:
|
||||
QMainWindow::keyPressEvent(event);
|
||||
}
|
||||
@@ -226,7 +503,7 @@ void MainWindow::updateStatusBar()
|
||||
info = QString("%1×%2 @ %3fps %4 ")
|
||||
.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,
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
statusBar()->showMessage("⚠ " + msg, 8000);
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
class VideoWidget;
|
||||
class CaptureThread;
|
||||
class AudioThread;
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QAction;
|
||||
class QLabel;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
@@ -15,6 +19,11 @@ public:
|
||||
MainWindow(const QString& videoDevice,
|
||||
const QString& audioDevice,
|
||||
int targetFps,
|
||||
bool smoothModeEnabled = true,
|
||||
bool retroModeEnabled = false,
|
||||
float brightness = 1.0f,
|
||||
float sharpenAmount = 0.0f,
|
||||
float lastSharpenAmount = 2.5f,
|
||||
QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
@@ -27,17 +36,37 @@ protected:
|
||||
private slots:
|
||||
void onError(const QString& msg);
|
||||
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 toggleSmoothMode();
|
||||
void toggleRetroMode();
|
||||
void toggleSharpen();
|
||||
void increaseSharpen();
|
||||
void decreaseSharpen();
|
||||
void increaseBrightness();
|
||||
void decreaseBrightness();
|
||||
void toggleRaw();
|
||||
void toggleCompare();
|
||||
void saveScreenshot();
|
||||
|
||||
private:
|
||||
void setupMenuBar();
|
||||
void toggleFullscreen();
|
||||
void stopAll();
|
||||
void updateStatusBar();
|
||||
void applyBrightness(float value);
|
||||
void applySharpen(float value);
|
||||
void showOverlayMessage(const QString& text);
|
||||
|
||||
VideoWidget* m_video = nullptr;
|
||||
CaptureThread* m_capture = nullptr;
|
||||
AudioThread* m_audio = nullptr;
|
||||
VideoWidget* m_video = nullptr;
|
||||
CaptureThread* m_capture = 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_audioDevice;
|
||||
|
||||
@@ -1,45 +1,450 @@
|
||||
#include "VideoWidget.h"
|
||||
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLTexture>
|
||||
#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 0–255-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)
|
||||
: QWidget(parent)
|
||||
: QOpenGLWidget(parent)
|
||||
{
|
||||
setMinimumSize(640, 360);
|
||||
// Schwarzer Hintergrund
|
||||
QPalette pal = palette();
|
||||
pal.setColor(QPalette::Window, Qt::black);
|
||||
setPalette(pal);
|
||||
setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
VideoWidget::~VideoWidget()
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (image.isNull()) return;
|
||||
m_pixmap = QPixmap::fromImage(image);
|
||||
m_pendingImage = image;
|
||||
m_pendingDirty = true;
|
||||
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);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
p.setPen(Qt::darkGray);
|
||||
p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
|
||||
}
|
||||
|
||||
if (m_pixmap.isNull()) {
|
||||
p.fillRect(rect(), Qt::black);
|
||||
p.setPen(Qt::darkGray);
|
||||
p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
|
||||
void VideoWidget::paintGL()
|
||||
{
|
||||
uploadPendingFrame();
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (!m_hasFrame || !m_texture) {
|
||||
drawNoSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Seitenverhältnis bewahren (letterbox/pillarbox)
|
||||
QRect target = rect();
|
||||
QSize scaled = m_pixmap.size().scaled(target.size(), Qt::KeepAspectRatio);
|
||||
QPoint topLeft(
|
||||
target.x() + (target.width() - scaled.width()) / 2,
|
||||
target.y() + (target.height() - scaled.height()) / 2
|
||||
);
|
||||
p.fillRect(rect(), Qt::black);
|
||||
p.drawPixmap(QRect(topLeft, scaled), m_pixmap, m_pixmap.rect());
|
||||
const float viewW = static_cast<float>(width());
|
||||
const float viewH = static_cast<float>(height());
|
||||
const float imgW = static_cast<float>(m_imageSize.width());
|
||||
const float imgH = static_cast<float>(m_imageSize.height());
|
||||
|
||||
float scaleX = 1.0f;
|
||||
float scaleY = 1.0f;
|
||||
if (imgW > 0.0f && imgH > 0.0f && viewW > 0.0f && viewH > 0.0f) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLExtraFunctions>
|
||||
#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
|
||||
|
||||
public:
|
||||
explicit VideoWidget(QWidget* parent = nullptr);
|
||||
~VideoWidget() override;
|
||||
|
||||
public slots:
|
||||
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:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void initializeGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void paintGL() override;
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
47
src/main.cpp
47
src/main.cpp
@@ -1,15 +1,24 @@
|
||||
#include <QApplication>
|
||||
#include <QSettings>
|
||||
#include <QMessageBox>
|
||||
#include <QSurfaceFormat>
|
||||
|
||||
#include "DeviceDialog.h"
|
||||
#include "MainWindow.h"
|
||||
|
||||
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);
|
||||
app.setApplicationName("HDMIViewer");
|
||||
app.setApplicationVersion("1.2");
|
||||
app.setApplicationVersion("1.4.0");
|
||||
app.setOrganizationName("hdmi-viewer");
|
||||
|
||||
QSettings settings;
|
||||
@@ -19,6 +28,12 @@ int main(int argc, char* argv[])
|
||||
dlg.setPreselectedVideo(settings.value("device/video").toString());
|
||||
dlg.setPreselectedAudio(settings.value("device/audio", "default").toString());
|
||||
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)
|
||||
break;
|
||||
@@ -26,6 +41,18 @@ int main(int argc, char* argv[])
|
||||
const QString videoDevice = dlg.selectedVideoDevice();
|
||||
const QString audioDevice = dlg.selectedAudioDevice();
|
||||
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()) {
|
||||
QMessageBox::critical(nullptr, "Fehler",
|
||||
@@ -36,9 +63,25 @@ int main(int argc, char* argv[])
|
||||
settings.setValue("device/video", videoDevice);
|
||||
settings.setValue("device/audio", audioDevice);
|
||||
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();
|
||||
|
||||
MainWindow win(videoDevice, audioDevice, fps);
|
||||
MainWindow win(videoDevice, audioDevice, fps, smoothMode, retroMode, brightness, sharpenAmt, lastSharpen);
|
||||
win.show();
|
||||
app.exec();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user