Erster Commit
This commit is contained in:
142
src/AudioThread.cpp
Normal file
142
src/AudioThread.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "AudioThread.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
AudioThread::AudioThread(const QString& device, QObject* parent)
|
||||
: QThread(parent), m_device(device)
|
||||
{}
|
||||
|
||||
AudioThread::~AudioThread()
|
||||
{
|
||||
stop();
|
||||
wait();
|
||||
}
|
||||
|
||||
void AudioThread::stop()
|
||||
{
|
||||
m_running = false;
|
||||
}
|
||||
|
||||
// ── ALSA-Hilfsfunktion ───────────────────────────────────────────────────────
|
||||
static bool setHwParams(snd_pcm_t* handle,
|
||||
unsigned int& rate, unsigned int& channels,
|
||||
snd_pcm_uframes_t periodFrames,
|
||||
QString& errMsg)
|
||||
{
|
||||
snd_pcm_hw_params_t* params;
|
||||
snd_pcm_hw_params_alloca(¶ms);
|
||||
snd_pcm_hw_params_any(handle, params);
|
||||
|
||||
auto check = [&](int rc, const char* what) -> bool {
|
||||
if (rc < 0) {
|
||||
errMsg = QString("%1: %2").arg(what).arg(snd_strerror(rc));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!check(snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED), "access")) return false;
|
||||
if (!check(snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE), "format")) return false;
|
||||
if (!check(snd_pcm_hw_params_set_channels_near(handle, params, &channels), "channels")) return false;
|
||||
if (!check(snd_pcm_hw_params_set_rate_near(handle, params, &rate, nullptr), "rate")) return false;
|
||||
|
||||
snd_pcm_uframes_t bufferFrames = periodFrames * 4;
|
||||
snd_pcm_hw_params_set_buffer_size_near(handle, params, &bufferFrames);
|
||||
snd_pcm_hw_params_set_period_size_near(handle, params, &periodFrames, nullptr);
|
||||
|
||||
if (!check(snd_pcm_hw_params(handle, params), "hw_params")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioThread::run()
|
||||
{
|
||||
m_running = true;
|
||||
|
||||
snd_pcm_t* captureHandle = nullptr;
|
||||
snd_pcm_t* playbackHandle = nullptr;
|
||||
|
||||
// ── Capture öffnen ───────────────────────────────────────────────────────
|
||||
QString captureDevice = m_device;
|
||||
// Falls der User ein hw:X-Device angibt, nutzen wir es direkt.
|
||||
// Sonst default: "default"
|
||||
int rc = snd_pcm_open(&captureHandle, captureDevice.toLocal8Bit().constData(),
|
||||
SND_PCM_STREAM_CAPTURE, 0);
|
||||
if (rc < 0) {
|
||||
emit errorOccurred(QString("Audio-Capture '%1' konnte nicht geöffnet werden: %2")
|
||||
.arg(captureDevice).arg(snd_strerror(rc)));
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int rate = k_defaultRate;
|
||||
unsigned int channels = k_defaultChannels;
|
||||
snd_pcm_uframes_t period = k_periodFrames;
|
||||
|
||||
QString errMsg;
|
||||
if (!setHwParams(captureHandle, rate, channels, period, errMsg)) {
|
||||
emit errorOccurred("Audio-Capture HW-Params: " + errMsg);
|
||||
snd_pcm_close(captureHandle);
|
||||
return;
|
||||
}
|
||||
snd_pcm_prepare(captureHandle);
|
||||
|
||||
qDebug() << "Audio Capture:" << rate << "Hz," << channels << "ch";
|
||||
|
||||
// ── Playback öffnen ──────────────────────────────────────────────────────
|
||||
rc = snd_pcm_open(&playbackHandle, "default", SND_PCM_STREAM_PLAYBACK, 0);
|
||||
if (rc < 0) {
|
||||
emit errorOccurred(QString("Audio-Playback konnte nicht geöffnet werden: %1")
|
||||
.arg(snd_strerror(rc)));
|
||||
snd_pcm_close(captureHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!setHwParams(playbackHandle, rate, channels, period, errMsg)) {
|
||||
emit errorOccurred("Audio-Playback HW-Params: " + errMsg);
|
||||
snd_pcm_close(captureHandle);
|
||||
snd_pcm_close(playbackHandle);
|
||||
return;
|
||||
}
|
||||
snd_pcm_prepare(playbackHandle);
|
||||
|
||||
// ── Loop: Capture → Playback ─────────────────────────────────────────────
|
||||
std::vector<qint16> buffer(period * channels);
|
||||
|
||||
while (m_running) {
|
||||
snd_pcm_sframes_t framesRead =
|
||||
snd_pcm_readi(captureHandle, buffer.data(), period);
|
||||
|
||||
if (framesRead < 0) {
|
||||
if (framesRead == -EPIPE) {
|
||||
snd_pcm_prepare(captureHandle);
|
||||
continue;
|
||||
}
|
||||
qWarning() << "Audio-Capture Fehler:" << snd_strerror(static_cast<int>(framesRead));
|
||||
// Kurz warten und weitermachen
|
||||
msleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
snd_pcm_sframes_t written = 0;
|
||||
while (written < framesRead && m_running) {
|
||||
snd_pcm_sframes_t w =
|
||||
snd_pcm_writei(playbackHandle,
|
||||
buffer.data() + written * static_cast<snd_pcm_sframes_t>(channels),
|
||||
static_cast<snd_pcm_uframes_t>(framesRead - written));
|
||||
if (w < 0) {
|
||||
if (w == -EPIPE) {
|
||||
snd_pcm_prepare(playbackHandle);
|
||||
break;
|
||||
}
|
||||
qWarning() << "Audio-Playback Fehler:" << snd_strerror(static_cast<int>(w));
|
||||
break;
|
||||
}
|
||||
written += w;
|
||||
}
|
||||
}
|
||||
|
||||
snd_pcm_drain(playbackHandle);
|
||||
snd_pcm_close(captureHandle);
|
||||
snd_pcm_close(playbackHandle);
|
||||
}
|
||||
36
src/AudioThread.h
Normal file
36
src/AudioThread.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QThread>
|
||||
#include <QString>
|
||||
#include <atomic>
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
class AudioThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioThread(const QString& device, QObject* parent = nullptr);
|
||||
~AudioThread() override;
|
||||
|
||||
void stop();
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString& message);
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
bool setupCapture(snd_pcm_t*& handle, unsigned int& rate, unsigned int& channels);
|
||||
bool setupPlayback(snd_pcm_t*& handle, unsigned int rate, unsigned int channels);
|
||||
|
||||
QString m_device;
|
||||
std::atomic<bool> m_running{false};
|
||||
|
||||
// PCM-Parameter
|
||||
static constexpr unsigned int k_defaultRate = 48000;
|
||||
static constexpr unsigned int k_defaultChannels = 2;
|
||||
static constexpr snd_pcm_uframes_t k_periodFrames = 1024;
|
||||
};
|
||||
358
src/CaptureThread.cpp
Normal file
358
src/CaptureThread.cpp
Normal file
@@ -0,0 +1,358 @@
|
||||
#include "CaptureThread.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
static int xioctl(int fd, unsigned long request, void* arg)
|
||||
{
|
||||
int r;
|
||||
do { r = ioctl(fd, request, arg); } while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── YUYV → RGB888 (BT.601 fixed-point) ──────────────────────────────────────
|
||||
|
||||
static inline quint8 clamp255(int v)
|
||||
{
|
||||
return static_cast<quint8>(static_cast<unsigned>(v) <= 255u ? v : v < 0 ? 0 : 255);
|
||||
}
|
||||
|
||||
// ── CaptureThread ────────────────────────────────────────────────────────────
|
||||
|
||||
CaptureThread::CaptureThread(const QString& device, int targetFps, QObject* parent)
|
||||
: QThread(parent), m_device(device), m_targetFps(targetFps)
|
||||
{}
|
||||
|
||||
CaptureThread::~CaptureThread()
|
||||
{
|
||||
stopAndWait();
|
||||
}
|
||||
|
||||
void CaptureThread::stopAndWait()
|
||||
{
|
||||
m_running = false;
|
||||
wait();
|
||||
}
|
||||
|
||||
void CaptureThread::run()
|
||||
{
|
||||
if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) {
|
||||
closeDevice();
|
||||
return;
|
||||
}
|
||||
|
||||
m_running = true;
|
||||
|
||||
fd_set fds;
|
||||
struct timeval tv;
|
||||
|
||||
while (m_running) {
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(m_fd, &fds);
|
||||
tv.tv_sec = 1;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int r = select(m_fd + 1, &fds, nullptr, nullptr, &tv);
|
||||
if (r == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
emit errorOccurred(QString("select(): %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
if (r == 0) continue;
|
||||
|
||||
v4l2_buffer buf{};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_DQBUF, &buf) == -1) {
|
||||
if (errno == EAGAIN) continue;
|
||||
emit errorOccurred(QString("VIDIOC_DQBUF: %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
|
||||
// Rohdaten kopieren
|
||||
QByteArray raw(reinterpret_cast<const char*>(m_buffers[buf.index].start),
|
||||
static_cast<qsizetype>(buf.bytesused));
|
||||
|
||||
// Buffer sofort zurückgeben
|
||||
if (xioctl(m_fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
emit errorOccurred(QString("VIDIOC_QBUF: %1").arg(strerror(errno)));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!m_running) break;
|
||||
|
||||
processFrame(raw.constData(), static_cast<size_t>(raw.size()));
|
||||
}
|
||||
|
||||
stopCapture();
|
||||
closeDevice();
|
||||
}
|
||||
|
||||
bool CaptureThread::openDevice()
|
||||
{
|
||||
m_fd = ::open(m_device.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK);
|
||||
if (m_fd == -1) {
|
||||
emit errorOccurred(QString("Kann %1 nicht öffnen: %2")
|
||||
.arg(m_device, strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
|
||||
v4l2_capability cap{};
|
||||
if (xioctl(m_fd, VIDIOC_QUERYCAP, &cap) == -1) {
|
||||
emit errorOccurred("VIDIOC_QUERYCAP fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
emit errorOccurred(m_device + " ist kein Video-Capture-Gerät");
|
||||
return false;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
|
||||
emit errorOccurred(m_device + " unterstützt kein Streaming");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CaptureThread::initDevice()
|
||||
{
|
||||
// Crop zurücksetzen
|
||||
v4l2_cropcap cc{};
|
||||
cc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(m_fd, VIDIOC_CROPCAP, &cc) == 0) {
|
||||
v4l2_crop crop{};
|
||||
crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
crop.c = cc.defrect;
|
||||
xioctl(m_fd, VIDIOC_S_CROP, &crop);
|
||||
}
|
||||
|
||||
// Formatpriorität: MJPEG → YUYV → NV12
|
||||
// MJPEG braucht viel weniger USB-Bandbreite → erlaubt 30fps bei FullHD
|
||||
struct { quint32 fmt; const char* name; } formats[] = {
|
||||
{ V4L2_PIX_FMT_MJPEG, "MJPEG" },
|
||||
{ V4L2_PIX_FMT_YUYV, "YUYV" },
|
||||
{ V4L2_PIX_FMT_NV12, "NV12" },
|
||||
};
|
||||
|
||||
bool ok = false;
|
||||
for (auto& f : formats) {
|
||||
v4l2_format fmt{};
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.width = 1920;
|
||||
fmt.fmt.pix.height = 1080;
|
||||
fmt.fmt.pix.pixelformat = f.fmt;
|
||||
fmt.fmt.pix.field = V4L2_FIELD_NONE;
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_S_FMT, &fmt) == 0) {
|
||||
m_width = static_cast<int>(fmt.fmt.pix.width);
|
||||
m_height = static_cast<int>(fmt.fmt.pix.height);
|
||||
m_pixelFormat = fmt.fmt.pix.pixelformat;
|
||||
qDebug() << "Format:" << m_width << "x" << m_height << f.name;
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
emit errorOccurred("Kein unterstütztes Pixelformat (MJPEG, YUYV, NV12)");
|
||||
return false;
|
||||
}
|
||||
|
||||
return initMmap();
|
||||
}
|
||||
|
||||
bool CaptureThread::setFramerate()
|
||||
{
|
||||
v4l2_streamparm parm{};
|
||||
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_G_PARM, &parm) == 0 &&
|
||||
(parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME))
|
||||
{
|
||||
parm.parm.capture.timeperframe.numerator = 1;
|
||||
parm.parm.capture.timeperframe.denominator = static_cast<quint32>(m_targetFps);
|
||||
xioctl(m_fd, VIDIOC_S_PARM, &parm);
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_G_PARM, &parm) == 0 &&
|
||||
parm.parm.capture.timeperframe.numerator > 0)
|
||||
{
|
||||
m_fps = static_cast<int>(parm.parm.capture.timeperframe.denominator /
|
||||
parm.parm.capture.timeperframe.numerator);
|
||||
}
|
||||
} else {
|
||||
m_fps = m_targetFps;
|
||||
}
|
||||
|
||||
char fmtStr[5] = {};
|
||||
memcpy(fmtStr, &m_pixelFormat, 4);
|
||||
emit captureInfo(m_width, m_height, m_fps,
|
||||
QString::fromLatin1(fmtStr).trimmed());
|
||||
|
||||
qDebug() << "Framerate:" << m_fps << "fps (gewünscht:" << m_targetFps << ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CaptureThread::initMmap()
|
||||
{
|
||||
v4l2_requestbuffers req{};
|
||||
req.count = 4;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_REQBUFS, &req) == -1) {
|
||||
emit errorOccurred("VIDIOC_REQBUFS fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
if (req.count < 2) {
|
||||
emit errorOccurred("Zu wenig Buffer-Speicher");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_buffers.resize(req.count);
|
||||
for (quint32 i = 0; i < req.count; ++i) {
|
||||
v4l2_buffer buf{};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
|
||||
if (xioctl(m_fd, VIDIOC_QUERYBUF, &buf) == -1) {
|
||||
emit errorOccurred("VIDIOC_QUERYBUF fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_buffers[i].length = buf.length;
|
||||
m_buffers[i].start = mmap(nullptr, buf.length,
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, m_fd, buf.m.offset);
|
||||
if (m_buffers[i].start == MAP_FAILED) {
|
||||
emit errorOccurred(QString("mmap: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CaptureThread::startCapture()
|
||||
{
|
||||
for (size_t i = 0; i < m_buffers.size(); ++i) {
|
||||
v4l2_buffer buf{};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = static_cast<quint32>(i);
|
||||
if (xioctl(m_fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
emit errorOccurred("VIDIOC_QBUF (init) fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(m_fd, VIDIOC_STREAMON, &type) == -1) {
|
||||
emit errorOccurred(QString("VIDIOC_STREAMON: %1").arg(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CaptureThread::stopCapture()
|
||||
{
|
||||
v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
xioctl(m_fd, VIDIOC_STREAMOFF, &type);
|
||||
}
|
||||
|
||||
void CaptureThread::closeDevice()
|
||||
{
|
||||
for (auto& b : m_buffers)
|
||||
if (b.start && b.start != MAP_FAILED)
|
||||
munmap(b.start, b.length);
|
||||
m_buffers.clear();
|
||||
|
||||
if (m_fd != -1) {
|
||||
::close(m_fd);
|
||||
m_fd = -1;
|
||||
qDebug() << "V4L2 freigegeben:" << m_device;
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureThread::processFrame(const void* data, size_t size)
|
||||
{
|
||||
QImage img;
|
||||
switch (m_pixelFormat) {
|
||||
case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break;
|
||||
case V4L2_PIX_FMT_YUYV: img = decodeYUYV(data); break;
|
||||
case V4L2_PIX_FMT_NV12: img = decodeNV12(data); break;
|
||||
default: return;
|
||||
}
|
||||
if (!img.isNull())
|
||||
emit frameReady(img);
|
||||
}
|
||||
|
||||
// ── MJPEG → RGB via Qt (kein externes libjpeg nötig) ────────────────────────
|
||||
// Qt's JPEG-Plugin nutzt intern libjpeg-turbo, wir vermeiden aber das direkte
|
||||
// Linking-Problem indem wir QImage::fromData() verwenden.
|
||||
|
||||
QImage CaptureThread::decodeMJPEG(const void* data, size_t size) const
|
||||
{
|
||||
const QByteArray ba(reinterpret_cast<const char*>(data),
|
||||
static_cast<qsizetype>(size));
|
||||
QImage img = QImage::fromData(ba, "JPEG");
|
||||
// Sicherstellen dass wir RGB888 haben (konsistent mit den anderen Dekodern)
|
||||
if (!img.isNull() && img.format() != QImage::Format_RGB888)
|
||||
img = img.convertToFormat(QImage::Format_RGB888);
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── YUYV → RGB888 ───────────────────────────────────────────────────────────
|
||||
|
||||
QImage CaptureThread::decodeYUYV(const void* src) const
|
||||
{
|
||||
QImage img(m_width, m_height, QImage::Format_RGB888);
|
||||
const quint8* in = reinterpret_cast<const quint8*>(src);
|
||||
|
||||
for (int y = 0; y < m_height; ++y) {
|
||||
quint8* line = img.scanLine(y);
|
||||
const quint8* row = in + y * m_width * 2;
|
||||
for (int x = 0; x < m_width; x += 2) {
|
||||
const int y0 = row[0], cb = row[1] - 128;
|
||||
const int y1 = row[2], cr = row[3] - 128;
|
||||
row += 4;
|
||||
const int rb = 1436 * cr, gb = -352 * cb - 731 * cr, bb = 1815 * cb;
|
||||
*line++ = clamp255((y0 * 1024 + rb) >> 10);
|
||||
*line++ = clamp255((y0 * 1024 + gb) >> 10);
|
||||
*line++ = clamp255((y0 * 1024 + bb) >> 10);
|
||||
*line++ = clamp255((y1 * 1024 + rb) >> 10);
|
||||
*line++ = clamp255((y1 * 1024 + gb) >> 10);
|
||||
*line++ = clamp255((y1 * 1024 + bb) >> 10);
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── NV12 → RGB888 ───────────────────────────────────────────────────────────
|
||||
|
||||
QImage CaptureThread::decodeNV12(const void* src) const
|
||||
{
|
||||
QImage img(m_width, m_height, QImage::Format_RGB888);
|
||||
const quint8* yp = reinterpret_cast<const quint8*>(src);
|
||||
const quint8* uvp = yp + m_width * m_height;
|
||||
|
||||
for (int row = 0; row < m_height; ++row) {
|
||||
quint8* line = img.scanLine(row);
|
||||
const quint8* y_row = yp + row * m_width;
|
||||
const quint8* uv_row = uvp + (row >> 1) * m_width;
|
||||
for (int col = 0; col < m_width; ++col) {
|
||||
const int yv = y_row[col];
|
||||
const int cb = uv_row[col & ~1] - 128;
|
||||
const int cr = uv_row[(col & ~1) + 1] - 128;
|
||||
*line++ = clamp255((yv * 1024 + 1436 * cr) >> 10);
|
||||
*line++ = clamp255((yv * 1024 - 352 * cb - 731 * cr) >> 10);
|
||||
*line++ = clamp255((yv * 1024 + 1815 * cb) >> 10);
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
63
src/CaptureThread.h
Normal file
63
src/CaptureThread.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <QThread>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
struct Buffer {
|
||||
void* start = nullptr;
|
||||
size_t length = 0;
|
||||
};
|
||||
|
||||
class CaptureThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CaptureThread(const QString& device, int targetFps,
|
||||
QObject* parent = nullptr);
|
||||
~CaptureThread() override;
|
||||
|
||||
void stopAndWait();
|
||||
|
||||
int captureWidth() const { return m_width; }
|
||||
int captureHeight() const { return m_height; }
|
||||
int captureFps() const { return m_fps; }
|
||||
|
||||
signals:
|
||||
void frameReady(QImage image);
|
||||
void errorOccurred(const QString& message);
|
||||
void captureInfo(int width, int height, int fps, const QString& format);
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
bool openDevice();
|
||||
bool initDevice();
|
||||
bool setFramerate();
|
||||
bool startCapture();
|
||||
void stopCapture();
|
||||
void closeDevice();
|
||||
bool initMmap();
|
||||
void processFrame(const void* data, size_t size);
|
||||
|
||||
// Dekodierung
|
||||
QImage decodeYUYV(const void* data) const;
|
||||
QImage decodeNV12(const void* data) const;
|
||||
QImage decodeMJPEG(const void* data, size_t size) const;
|
||||
|
||||
QString m_device;
|
||||
int m_fd = -1;
|
||||
int m_width = 1920;
|
||||
int m_height = 1080;
|
||||
int m_fps = 30;
|
||||
int m_targetFps = 30;
|
||||
quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG;
|
||||
std::vector<Buffer> m_buffers;
|
||||
std::atomic<bool> m_running{false};
|
||||
};
|
||||
148
src/DeviceDialog.cpp
Normal file
148
src/DeviceDialog.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "DeviceDialog.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
static int xioctl(int fd, unsigned long req, void* arg)
|
||||
{
|
||||
int r;
|
||||
do { r = ioctl(fd, req, arg); } while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
DeviceDialog::DeviceDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("HDMI Viewer – Gerät auswählen");
|
||||
setMinimumWidth(520);
|
||||
|
||||
auto* form = new QFormLayout;
|
||||
m_videoCombo = new QComboBox(this);
|
||||
m_audioCombo = new QComboBox(this);
|
||||
m_fpsCombo = new QComboBox(this);
|
||||
|
||||
m_videoCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
m_audioCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
|
||||
populateVideoDevices();
|
||||
populateAudioDevices();
|
||||
|
||||
// FPS-Optionen: gängige Werte für HDMI-Grabber
|
||||
for (int fps : {10, 15, 20, 24, 25, 30, 50, 60})
|
||||
m_fpsCombo->addItem(QString::number(fps) + " fps", fps);
|
||||
// Default: 30
|
||||
setPreselectedFps(30);
|
||||
|
||||
form->addRow("Video-Gerät (V4L2):", m_videoCombo);
|
||||
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
||||
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
||||
|
||||
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>",
|
||||
this);
|
||||
hint->setWordWrap(true);
|
||||
|
||||
auto* btnBox = new QHBoxLayout;
|
||||
auto* btnOk = new QPushButton("Starten", this);
|
||||
auto* btnClose = new QPushButton("Beenden", this);
|
||||
btnOk->setDefault(true);
|
||||
btnBox->addStretch();
|
||||
btnBox->addWidget(btnClose);
|
||||
btnBox->addWidget(btnOk);
|
||||
|
||||
connect(btnOk, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(btnClose, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
auto* main = new QVBoxLayout(this);
|
||||
main->addLayout(form);
|
||||
main->addSpacing(8);
|
||||
main->addWidget(hint);
|
||||
main->addSpacing(12);
|
||||
main->addLayout(btnBox);
|
||||
}
|
||||
|
||||
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(); }
|
||||
|
||||
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::selectByData(QComboBox* combo, const QString& data)
|
||||
{
|
||||
for (int i = 0; i < combo->count(); ++i)
|
||||
if (combo->itemData(i).toString() == data) { combo->setCurrentIndex(i); return; }
|
||||
}
|
||||
|
||||
void DeviceDialog::populateVideoDevices()
|
||||
{
|
||||
QDir dev("/dev");
|
||||
const QStringList entries = dev.entryList({"video*"}, QDir::System, QDir::Name);
|
||||
|
||||
for (const QString& entry : entries) {
|
||||
const QString path = "/dev/" + entry;
|
||||
int fd = ::open(path.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK);
|
||||
if (fd == -1) continue;
|
||||
|
||||
v4l2_capability cap{};
|
||||
QString label = path;
|
||||
if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { ::close(fd); continue; }
|
||||
const QString card = QString::fromLatin1(
|
||||
reinterpret_cast<const char*>(cap.card), 32).trimmed();
|
||||
if (!card.isEmpty())
|
||||
label = QString("%1 [%2]").arg(path, card);
|
||||
}
|
||||
::close(fd);
|
||||
m_videoCombo->addItem(label, path);
|
||||
}
|
||||
|
||||
if (m_videoCombo->count() == 0)
|
||||
m_videoCombo->addItem("Kein V4L2-Gerät gefunden", "");
|
||||
}
|
||||
|
||||
void DeviceDialog::populateAudioDevices()
|
||||
{
|
||||
m_audioCombo->addItem("default [System-Standard]", "default");
|
||||
|
||||
void** hints = nullptr;
|
||||
if (snd_device_name_hint(-1, "pcm", &hints) < 0) return;
|
||||
|
||||
for (void** hint = hints; *hint != nullptr; ++hint) {
|
||||
char* ioid = snd_device_name_get_hint(*hint, "IOID");
|
||||
const bool isCapture = (ioid == nullptr || strcmp(ioid, "Input") == 0);
|
||||
free(ioid);
|
||||
if (!isCapture) continue;
|
||||
|
||||
char* name = snd_device_name_get_hint(*hint, "NAME");
|
||||
char* desc = snd_device_name_get_hint(*hint, "DESC");
|
||||
|
||||
if (name) {
|
||||
const QString n = QString::fromLatin1(name);
|
||||
const QString d = desc ? QString::fromLatin1(desc).replace('\n', ' ') : n;
|
||||
if (n != "default")
|
||||
m_audioCombo->addItem(QString("%1 [%2]").arg(n, d.left(60)), n);
|
||||
free(name);
|
||||
}
|
||||
if (desc) free(desc);
|
||||
}
|
||||
snd_device_name_free_hint(hints);
|
||||
}
|
||||
33
src/DeviceDialog.h
Normal file
33
src/DeviceDialog.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QComboBox;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class DeviceDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeviceDialog(QWidget* parent = nullptr);
|
||||
|
||||
QString selectedVideoDevice() const;
|
||||
QString selectedAudioDevice() const;
|
||||
int selectedFps() const;
|
||||
|
||||
void setPreselectedVideo(const QString& device);
|
||||
void setPreselectedAudio(const QString& device);
|
||||
void setPreselectedFps(int fps);
|
||||
|
||||
private:
|
||||
void populateVideoDevices();
|
||||
void populateAudioDevices();
|
||||
void selectByData(QComboBox* combo, const QString& data);
|
||||
|
||||
QComboBox* m_videoCombo = nullptr;
|
||||
QComboBox* m_audioCombo = nullptr;
|
||||
QComboBox* m_fpsCombo = nullptr;
|
||||
};
|
||||
251
src/MainWindow.cpp
Normal file
251
src/MainWindow.cpp
Normal file
@@ -0,0 +1,251 @@
|
||||
#include "MainWindow.h"
|
||||
#include "VideoWidget.h"
|
||||
#include "CaptureThread.h"
|
||||
#include "AudioThread.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QCloseEvent>
|
||||
#include <QStatusBar>
|
||||
#include <QMenuBar>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QFrame>
|
||||
|
||||
MainWindow::MainWindow(const QString& videoDevice,
|
||||
const QString& audioDevice,
|
||||
int targetFps,
|
||||
QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
, m_videoDevice(videoDevice)
|
||||
, m_audioDevice(audioDevice)
|
||||
{
|
||||
setWindowTitle("HDMI Viewer");
|
||||
resize(1280, 720);
|
||||
|
||||
m_video = new VideoWidget(this);
|
||||
setCentralWidget(m_video);
|
||||
|
||||
setupMenuBar();
|
||||
updateStatusBar();
|
||||
|
||||
// ── Capture ──────────────────────────────────────────────────────────────
|
||||
m_capture = new CaptureThread(videoDevice, targetFps, this);
|
||||
|
||||
connect(m_capture, &CaptureThread::frameReady,
|
||||
m_video, &VideoWidget::setFrame,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_capture, &CaptureThread::errorOccurred,
|
||||
this, &MainWindow::onError,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_capture, &CaptureThread::captureInfo,
|
||||
this, &MainWindow::onCaptureInfo,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_capture->start();
|
||||
|
||||
// ── Audio ─────────────────────────────────────────────────────────────────
|
||||
if (!audioDevice.isEmpty()) {
|
||||
m_audio = new AudioThread(audioDevice, this);
|
||||
connect(m_audio, &AudioThread::errorOccurred,
|
||||
this, &MainWindow::onError,
|
||||
Qt::QueuedConnection);
|
||||
m_audio->start();
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
stopAll();
|
||||
}
|
||||
|
||||
void MainWindow::setupMenuBar()
|
||||
{
|
||||
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
||||
|
||||
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
||||
aboutAct->setMenuRole(QAction::AboutRole);
|
||||
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
||||
}
|
||||
|
||||
void MainWindow::showAbout()
|
||||
{
|
||||
auto* dlg = new QDialog(this);
|
||||
dlg->setWindowTitle("Über HDMI Viewer");
|
||||
dlg->setFixedSize(420, 340);
|
||||
dlg->setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(dlg);
|
||||
root->setSpacing(0);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// ── Header-Bereich ────────────────────────────────────────────────────────
|
||||
auto* header = new QWidget(dlg);
|
||||
header->setFixedHeight(90);
|
||||
header->setStyleSheet("background-color: #1e1e2e;");
|
||||
auto* headerLayout = new QVBoxLayout(header);
|
||||
headerLayout->setContentsMargins(24, 16, 24, 16);
|
||||
|
||||
auto* titleLabel = new QLabel("HDMI Viewer", header);
|
||||
titleLabel->setStyleSheet("font-size: 22px; font-weight: bold; color: #cdd6f4;");
|
||||
|
||||
auto* subtitleLabel = new QLabel("Live-Vorschau für HDMI-Grabber", header);
|
||||
subtitleLabel->setStyleSheet("font-size: 11px; color: #6c7086;");
|
||||
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addWidget(subtitleLabel);
|
||||
|
||||
// ── Trennlinie ────────────────────────────────────────────────────────────
|
||||
auto* sep = new QFrame(dlg);
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setStyleSheet("color: #313244;");
|
||||
|
||||
// ── Info-Bereich ──────────────────────────────────────────────────────────
|
||||
auto* body = new QWidget(dlg);
|
||||
auto* grid = new QGridLayout(body);
|
||||
grid->setContentsMargins(24, 20, 24, 8);
|
||||
grid->setVerticalSpacing(10);
|
||||
grid->setHorizontalSpacing(16);
|
||||
grid->setColumnMinimumWidth(0, 100);
|
||||
|
||||
auto addRow = [&](int row, const QString& label, const QString& value) {
|
||||
auto* l = new QLabel(label, body);
|
||||
l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
l->setStyleSheet("font-weight: bold; color: #a6adc8;");
|
||||
|
||||
auto* v = new QLabel(value, body);
|
||||
v->setStyleSheet("color: #cdd6f4;");
|
||||
v->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
grid->addWidget(l, row, 0);
|
||||
grid->addWidget(v, row, 1);
|
||||
};
|
||||
|
||||
addRow(0, "Version", "1.3.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");
|
||||
|
||||
// ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
|
||||
auto* sep2 = new QFrame(body);
|
||||
sep2->setFrameShape(QFrame::HLine);
|
||||
sep2->setStyleSheet("color: #313244;");
|
||||
grid->addWidget(sep2, 7, 0, 1, 2);
|
||||
|
||||
auto* keysLabel = new QLabel(
|
||||
"<small><b>F</b> Vollbild ein/aus "
|
||||
"<b>Esc</b> Zurück zu Einstellungen</small>", body);
|
||||
keysLabel->setStyleSheet("color: #6c7086;");
|
||||
keysLabel->setAlignment(Qt::AlignCenter);
|
||||
grid->addWidget(keysLabel, 8, 0, 1, 2);
|
||||
|
||||
// ── Schließen-Button ──────────────────────────────────────────────────────
|
||||
auto* btnRow = new QHBoxLayout;
|
||||
btnRow->setContentsMargins(24, 8, 24, 20);
|
||||
auto* closeBtn = new QPushButton("Schließen", dlg);
|
||||
closeBtn->setDefault(true);
|
||||
closeBtn->setFixedWidth(100);
|
||||
btnRow->addStretch();
|
||||
btnRow->addWidget(closeBtn);
|
||||
connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept);
|
||||
|
||||
root->addWidget(header);
|
||||
root->addWidget(sep);
|
||||
root->addWidget(body, 1);
|
||||
root->addLayout(btnRow);
|
||||
|
||||
dlg->exec();
|
||||
dlg->deleteLater();
|
||||
}
|
||||
|
||||
void MainWindow::stopAll()
|
||||
{
|
||||
if (m_capture) {
|
||||
m_capture->stopAndWait();
|
||||
delete m_capture;
|
||||
m_capture = nullptr;
|
||||
}
|
||||
if (m_audio) {
|
||||
m_audio->stop();
|
||||
m_audio->wait();
|
||||
delete m_audio;
|
||||
m_audio = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
switch (event->key()) {
|
||||
case Qt::Key_F:
|
||||
toggleFullscreen();
|
||||
break;
|
||||
case Qt::Key_Escape:
|
||||
if (m_fullscreen) {
|
||||
toggleFullscreen();
|
||||
} else {
|
||||
m_wantsReopen = true;
|
||||
close();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
QMainWindow::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
stopAll();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void MainWindow::toggleFullscreen()
|
||||
{
|
||||
if (m_fullscreen) {
|
||||
showNormal();
|
||||
menuBar()->show();
|
||||
statusBar()->show();
|
||||
} else {
|
||||
showFullScreen();
|
||||
menuBar()->hide();
|
||||
statusBar()->hide();
|
||||
}
|
||||
m_fullscreen = !m_fullscreen;
|
||||
}
|
||||
|
||||
void MainWindow::updateStatusBar()
|
||||
{
|
||||
QString info;
|
||||
if (m_capWidth > 0)
|
||||
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")
|
||||
.arg(m_videoDevice,
|
||||
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
||||
|
||||
statusBar()->showMessage(info);
|
||||
}
|
||||
|
||||
void MainWindow::onCaptureInfo(int width, int height, int fps, const QString& format)
|
||||
{
|
||||
m_capWidth = width;
|
||||
m_capHeight = height;
|
||||
m_capFps = fps;
|
||||
m_capFmt = format;
|
||||
updateStatusBar();
|
||||
setWindowTitle(QString("HDMI Viewer – %1×%2 @ %3fps %4")
|
||||
.arg(width).arg(height).arg(fps).arg(format));
|
||||
}
|
||||
|
||||
void MainWindow::onError(const QString& msg)
|
||||
{
|
||||
statusBar()->showMessage("⚠ " + msg, 8000);
|
||||
qWarning() << "[Error]" << msg;
|
||||
}
|
||||
52
src/MainWindow.h
Normal file
52
src/MainWindow.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QString>
|
||||
|
||||
class VideoWidget;
|
||||
class CaptureThread;
|
||||
class AudioThread;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(const QString& videoDevice,
|
||||
const QString& audioDevice,
|
||||
int targetFps,
|
||||
QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
bool wantsReopen() const { return m_wantsReopen; }
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void onError(const QString& msg);
|
||||
void onCaptureInfo(int width, int height, int fps, const QString& format);
|
||||
void showAbout();
|
||||
|
||||
private:
|
||||
void setupMenuBar();
|
||||
void toggleFullscreen();
|
||||
void stopAll();
|
||||
void updateStatusBar();
|
||||
|
||||
VideoWidget* m_video = nullptr;
|
||||
CaptureThread* m_capture = nullptr;
|
||||
AudioThread* m_audio = nullptr;
|
||||
|
||||
QString m_videoDevice;
|
||||
QString m_audioDevice;
|
||||
|
||||
bool m_fullscreen = false;
|
||||
bool m_wantsReopen = false;
|
||||
|
||||
int m_capWidth = 0;
|
||||
int m_capHeight = 0;
|
||||
int m_capFps = 0;
|
||||
QString m_capFmt;
|
||||
};
|
||||
45
src/VideoWidget.cpp
Normal file
45
src/VideoWidget.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "VideoWidget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
|
||||
VideoWidget::VideoWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setMinimumSize(640, 360);
|
||||
// Schwarzer Hintergrund
|
||||
QPalette pal = palette();
|
||||
pal.setColor(QPalette::Window, Qt::black);
|
||||
setPalette(pal);
|
||||
setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
void VideoWidget::setFrame(const QImage& image)
|
||||
{
|
||||
if (image.isNull()) return;
|
||||
m_pixmap = QPixmap::fromImage(image);
|
||||
update();
|
||||
}
|
||||
|
||||
void VideoWidget::paintEvent(QPaintEvent* /*event*/)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
|
||||
if (m_pixmap.isNull()) {
|
||||
p.fillRect(rect(), Qt::black);
|
||||
p.setPen(Qt::darkGray);
|
||||
p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
|
||||
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());
|
||||
}
|
||||
22
src/VideoWidget.h
Normal file
22
src/VideoWidget.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
|
||||
class VideoWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VideoWidget(QWidget* parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void setFrame(const QImage& image);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
QPixmap m_pixmap;
|
||||
};
|
||||
52
src/main.cpp
Normal file
52
src/main.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <QApplication>
|
||||
#include <QSettings>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "DeviceDialog.h"
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("HDMIViewer");
|
||||
app.setApplicationVersion("1.2");
|
||||
app.setOrganizationName("hdmi-viewer");
|
||||
|
||||
QSettings settings;
|
||||
|
||||
while (true) {
|
||||
DeviceDialog dlg;
|
||||
dlg.setPreselectedVideo(settings.value("device/video").toString());
|
||||
dlg.setPreselectedAudio(settings.value("device/audio", "default").toString());
|
||||
dlg.setPreselectedFps(settings.value("device/fps", 30).toInt());
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
break;
|
||||
|
||||
const QString videoDevice = dlg.selectedVideoDevice();
|
||||
const QString audioDevice = dlg.selectedAudioDevice();
|
||||
const int fps = dlg.selectedFps();
|
||||
|
||||
if (videoDevice.isEmpty()) {
|
||||
QMessageBox::critical(nullptr, "Fehler",
|
||||
"Kein gültiges Video-Gerät ausgewählt.");
|
||||
continue;
|
||||
}
|
||||
|
||||
settings.setValue("device/video", videoDevice);
|
||||
settings.setValue("device/audio", audioDevice);
|
||||
settings.setValue("device/fps", fps);
|
||||
settings.sync();
|
||||
|
||||
MainWindow win(videoDevice, audioDevice, fps);
|
||||
win.show();
|
||||
app.exec();
|
||||
|
||||
if (!win.wantsReopen())
|
||||
break;
|
||||
// Esc gedrückt → Schleife weiter, Dialog öffnet neu
|
||||
// V4L2-Device ist hier garantiert freigegeben (stopAll in closeEvent)
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user