From eb11d2ebc5731c0aee54ffc630061da1c2020347 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 22:33:59 +0200 Subject: [PATCH] Carry native camera modes through Full HD capture protocol --- tools/chatgui/src/call_camera_capture.h | 9 ++++- tools/chatgui/src/call_camera_protocol.h | 8 ++++- tools/chatgui/src/call_camera_worker.cpp | 44 ++++++++++++++++++------ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/tools/chatgui/src/call_camera_capture.h b/tools/chatgui/src/call_camera_capture.h index 9636fc81..d2a2f994 100644 --- a/tools/chatgui/src/call_camera_capture.h +++ b/tools/chatgui/src/call_camera_capture.h @@ -2,8 +2,15 @@ #include #include #include +#include "stream_video_modes.h" -using CallCameraDevices = QList>; +// Только аппаратные режимы камеры, пригодные для звонка; опрос выполняется в процессе захвата. +struct CallCameraDevice { + QString name; + QByteArray id; + QList modes; +}; +using CallCameraDevices = QList; constexpr qint64 CallCameraMaxFrameAgeUs = 250000; // GUI получает только собственную CPU-память. Процесс захвата завершается асинхронно. diff --git a/tools/chatgui/src/call_camera_protocol.h b/tools/chatgui/src/call_camera_protocol.h index 529d32b3..4c61869f 100644 --- a/tools/chatgui/src/call_camera_protocol.h +++ b/tools/chatgui/src/call_camera_protocol.h @@ -6,9 +6,15 @@ namespace CallCameraProtocol { enum Type : quint8 { Configure = 1, Quit, Ack, Devices, Frame, Stage, Error, Buffer }; -constexpr quint32 FrameBytes = 1280 * 720 * 4; +constexpr quint32 FrameBytes = 1920 * 1080 * 4; constexpr quint32 MaxPacket = 65536; +// Full HD в обеих ориентациях; чётные размеры необходимы для YUV420. +inline bool validSize(int width, int height) { + return width >= 16 && height >= 16 && width <= 1920 && height <= 1920 + && (width <= 1080 || height <= 1080) && !(width % 2) && !(height % 2); +} + // Только команды и метаданные. Пиксели — в mapping с владением Frame -> Ack, без shared mutex. inline QByteArray packet(Type type, quint64 generation, const std::function& write = {}) { QByteArray result; diff --git a/tools/chatgui/src/call_camera_worker.cpp b/tools/chatgui/src/call_camera_worker.cpp index 731b2242..29f6e6ab 100644 --- a/tools/chatgui/src/call_camera_worker.cpp +++ b/tools/chatgui/src/call_camera_worker.cpp @@ -30,7 +30,20 @@ public: ~QtCallCameraSource() override { stop(); } CallCameraDevices devices() override { CallCameraDevices result; - for (const auto& device : QMediaDevices::videoInputs()) result.append({device.description(), device.id()}); + for (const auto& device : QMediaDevices::videoInputs()) { + CallCameraDevice entry{device.description(), device.id(), {}}; + for (const auto& format : device.videoFormats()) { + const QSize size = format.resolution(); + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "camera capability device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", + device.id().constData(), size.width(), size.height(), format.pixelFormat(), format.minFrameRate(), format.maxFrameRate()); + if (validSize(size.width(), size.height())) + stream_video_add_camera_modes(entry.modes, size, format.minFrameRate(), format.maxFrameRate()); + } + entry.modes.removeIf([](const StreamVideoMode& mode) { return mode.fps != 30; }); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera modes device=%s count=%lld limit=1920x1080 fps=30", + device.id().constData(), (long long)entry.modes.size()); + result.append(entry); + } return result; } bool start(const QByteArray& device, int width, int height) override { @@ -40,13 +53,17 @@ public: session = std::make_unique(); sink = std::make_unique(); camera = std::make_unique(selected); session->setCamera(camera.get()); session->setVideoSink(sink.get()); - QCameraFormat best; int distance = INT_MAX; + QCameraFormat best; for (const auto& format : selected.videoFormats()) { - if (format.maxFrameRate() < 29.9) continue; - int score = std::abs(format.resolution().width() - width) + std::abs(format.resolution().height() - height); - if (score < distance) { best = format; distance = score; } + if (!stream_video_camera_supports(format.resolution(), format.minFrameRate(), format.maxFrameRate(), + {QSize(width, height), 30})) continue; + if (best.isNull() || format.maxFrameRate() < best.maxFrameRate()) best = format; } - if (!best.isNull()) camera->setCameraFormat(best); + if (best.isNull()) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera mode unavailable device=%s size=%dx%d fps=30", device.constData(), width, height); + emit error(QStringLiteral("Selected camera mode is unavailable")); return false; + } + camera->setCameraFormat(best); const auto format = camera->cameraFormat(); DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera selected device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", device.constData(), format.resolution().width(), format.resolution().height(), format.pixelFormat(), @@ -123,11 +140,14 @@ class CameraWorker : public QObject { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera frame conversion failed"); send(packet(Error, generation, [](QDataStream& out) { out << QStringLiteral("Camera frame conversion failed"); })); return; } - if (image.width() > width || image.height() > height) image = image.scaled(width, height, Qt::KeepAspectRatio, Qt::FastTransformation); + if (image.size() != QSize(width, height)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera geometry changed requested=%dx%d actual=%dx%d", width, height, image.width(), image.height()); + send(packet(Error, generation, [](QDataStream& out) { out << QStringLiteral("Camera changed the selected resolution"); })); return; + } image = image.convertToFormat(QImage::Format_RGB32); const qint64 converted = call_video_playout_now_us(); if (converted - pts <= CallCameraMaxFrameAgeUs) { - std::memcpy(pixels, image.constBits(), size_t(image.width()) * image.height() * 4); + for (int row = 0; row < height; ++row) std::memcpy(pixels + size_t(row) * width * 4, image.constScanLine(row), size_t(width) * 4); std::atomic_thread_fence(std::memory_order_release); auto bytes = packet(Frame, generation, [&](QDataStream& out) { out << pts << qint64(clock.correction) << qint32(image.width()) << qint32(image.height()); @@ -155,7 +175,7 @@ class CameraWorker : public QObject { if (sending && revision == inFlight) { sending = false; if (pending) convert(pending, generation); } } else if (type == Configure) { bool enabled = false; QByteArray device; qint32 w = 0, h = 0; in >> enabled >> device >> w >> h; - if (in.status() != QDataStream::Ok || w < 1 || h < 1 || w > 1280 || h > 720 || !pixels) return false; + if (in.status() != QDataStream::Ok || !validSize(w, h) || !pixels) return false; stop(); generation = revision; width = w; height = h; clock = {}; cadence = {}; lastPts = -1; if (!enabled) { stage(QStringLiteral("idle")); return true; } stage(QStringLiteral("start")); @@ -185,7 +205,11 @@ public: stage(QStringLiteral("enumerate")); const auto devices = source->devices(); send(packet(Devices, 0, [&](QDataStream& out) { - out << quint32(devices.size()); for (const auto& device : devices) out << device.first << device.second; + out << quint32(devices.size()); + for (const auto& device : devices) { + out << device.name << device.id << quint32(device.modes.size()); + for (const auto& mode : device.modes) out << qint32(mode.size.width()) << qint32(mode.size.height()); + } })); stage(QStringLiteral("idle")); });