From 7191581c1b86423db42aa8e8e0ad2f9eac57c652 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Wed, 24 Sep 2025 15:23:42 +0000 Subject: [PATCH] WIP: Add V4L2 --- src/airplaywindow.cpp | 157 +++++++++++++++++++++++++++++++++++++++++- src/airplaywindow.h | 13 ++++ 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/src/airplaywindow.cpp b/src/airplaywindow.cpp index 8daaae4..958064e 100644 --- a/src/airplaywindow.cpp +++ b/src/airplaywindow.cpp @@ -1,11 +1,22 @@ #include "airplaywindow.h" #include +#include #include #include +#include +#include #include #include #include +// V4L2 includes +#include +#include +#include +#include +#include +#include + // Include the rpiplay server functions extern "C" { int start_server_qt(const char *name); @@ -17,12 +28,18 @@ std::function qt_video_callback; AirPlayWindow::AirPlayWindow(QWidget *parent) : QMainWindow(parent), m_videoLabel(nullptr), m_statusLabel(nullptr), - m_serverThread(nullptr), m_serverRunning(false) + m_serverThread(nullptr), m_serverRunning(false), m_v4l2_fd(-1), + m_v4l2_width(0), m_v4l2_height(0), m_v4l2_enabled(false) { setupUI(); // Setup video callback qt_video_callback = [this](uint8_t *data, int width, int height) { + // V4L2 output if enabled + if (m_v4l2_enabled) { + writeFrameToV4L2(data, width, height); + } + QByteArray frameData((const char *)data, width * height * 3); QMetaObject::invokeMethod(this, "updateVideoFrame", Qt::QueuedConnection, @@ -34,6 +51,7 @@ AirPlayWindow::AirPlayWindow(QWidget *parent) AirPlayWindow::~AirPlayWindow() { stopAirPlayServer(); + closeV4L2(); qt_video_callback = nullptr; } @@ -63,6 +81,24 @@ void AirPlayWindow::setupUI() statusLayout->addWidget(m_statusLabel); statusLayout->addStretch(); + + // V4L2 controls + QCheckBox *v4l2CheckBox = new QCheckBox("Enable V4L2 Output"); + connect(v4l2CheckBox, &QCheckBox::toggled, this, [this](bool enabled) { + m_v4l2_enabled = enabled; + if (!enabled) { + closeV4L2(); + } + qDebug() << "V4L2 output" << (enabled ? "enabled" : "disabled"); + }); + + QPushButton *testV4L2Btn = new QPushButton("Test V4L2"); + testV4L2Btn->setToolTip("Test V4L2 loopback device availability"); + connect(testV4L2Btn, &QPushButton::clicked, this, + [this]() { testV4L2Device(); }); + + statusLayout->addWidget(v4l2CheckBox); + statusLayout->addWidget(testV4L2Btn); statusLayout->addWidget(startBtn); statusLayout->addWidget(stopBtn); @@ -153,3 +189,122 @@ void AirPlayServerThread::run() stop_server_qt(); emit statusChanged(false); } + +// V4L2 Implementation +void AirPlayWindow::initV4L2(int width, int height, const char *device) +{ + closeV4L2(); // Close previous device if any + + m_v4l2_fd = open(device, O_WRONLY); + if (m_v4l2_fd < 0) { + qWarning("Failed to open V4L2 device %s: %s", device, strerror(errno)); + QMessageBox::warning( + this, "V4L2 Error", + QString("Failed to open V4L2 device %1.\n" + "Make sure v4l2loopback module is loaded:\n" + "sudo modprobe v4l2loopback") + .arg(device)); + return; + } + + struct v4l2_format fmt; + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; + fmt.fmt.pix.width = width; + fmt.fmt.pix.height = height; + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; + fmt.fmt.pix.field = V4L2_FIELD_NONE; + fmt.fmt.pix.bytesperline = width * 3; + fmt.fmt.pix.sizeimage = (unsigned int)width * height * 3; + + if (ioctl(m_v4l2_fd, VIDIOC_S_FMT, &fmt) < 0) { + qWarning("Failed to set V4L2 format: %s", strerror(errno)); + ::close(m_v4l2_fd); + m_v4l2_fd = -1; + QMessageBox::warning( + this, "V4L2 Error", + "Failed to set V4L2 video format. Device may not support RGB24."); + return; + } + + m_v4l2_width = width; + m_v4l2_height = height; + qDebug("V4L2 device %s initialized to %dx%d", device, width, height); +} + +void AirPlayWindow::closeV4L2() +{ + if (m_v4l2_fd >= 0) { + ::close(m_v4l2_fd); + m_v4l2_fd = -1; + qDebug("V4L2 device closed."); + } +} + +void AirPlayWindow::writeFrameToV4L2(uint8_t *data, int width, int height) +{ + // Check if V4L2 device needs to be initialized or re-initialized + if (m_v4l2_fd < 0 || m_v4l2_width != width || m_v4l2_height != height) { + initV4L2(width, height, "/dev/video0"); // Use your v4l2loopback device + } + + // Write frame to V4L2 device if it's open + if (m_v4l2_fd >= 0) { + ssize_t bytes_written = + write(m_v4l2_fd, data, (size_t)width * height * 3); + if (bytes_written < 0) { + qWarning("Failed to write frame to V4L2 device: %s", + strerror(errno)); + closeV4L2(); // Close on error to retry initialization + } + } +} + +void AirPlayWindow::testV4L2Device() +{ + const char *device = "/dev/video0"; + int fd = open(device, O_WRONLY); + + if (fd < 0) { + QMessageBox::critical(this, "V4L2 Test", + QString("Failed to open V4L2 device %1.\n" + "Error: %2\n\n" + "To fix this, run:\n" + "sudo modprobe v4l2loopback video_nr=0") + .arg(device) + .arg(strerror(errno))); + return; + } + + // Test if device supports V4L2_PIX_FMT_RGB24 + struct v4l2_format fmt; + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; + fmt.fmt.pix.width = 1280; + fmt.fmt.pix.height = 720; + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; + fmt.fmt.pix.field = V4L2_FIELD_NONE; + fmt.fmt.pix.bytesperline = 1280 * 3; + fmt.fmt.pix.sizeimage = 1280 * 720 * 3; + + if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { + ::close(fd); + QMessageBox::warning( + this, "V4L2 Test", + QString("V4L2 device %1 exists but doesn't support RGB24 format.\n" + "Error: %2") + .arg(device) + .arg(strerror(errno))); + return; + } + + ::close(fd); + QMessageBox::information( + this, "V4L2 Test", + QString("✓ V4L2 device %1 is working correctly!\n\n" + "You can now:\n" + "• View output with: ffplay %1\n" + "• Use in OBS as Video Capture Device\n" + "• Record with: ffmpeg -f v4l2 -i %1 output.mp4") + .arg(device)); +} diff --git a/src/airplaywindow.h b/src/airplaywindow.h index dffa24f..301673e 100644 --- a/src/airplaywindow.h +++ b/src/airplaywindow.h @@ -5,6 +5,7 @@ #include #include #include +#include #include class AirPlayServerThread; @@ -32,6 +33,18 @@ private: QLabel *m_statusLabel; AirPlayServerThread *m_serverThread; bool m_serverRunning; + + // V4L2 members + int m_v4l2_fd; + int m_v4l2_width; + int m_v4l2_height; + bool m_v4l2_enabled; + + // V4L2 methods + void initV4L2(int width, int height, const char *device); + void closeV4L2(); + void writeFrameToV4L2(uint8_t *data, int width, int height); + void testV4L2Device(); }; class AirPlayServerThread : public QThread