Carry native camera modes through Full HD capture protocol

This commit is contained in:
evgeny committed 2026-10-19 22:33:59 +02:00
1 parent 9a3ef7b622
commit eb11d2ebc5
3 files changed
+49 -12

No files matched your search

+8 -1
View File
@@ -2,8 +2,15 @@
#include <QObject>
#include <QImage>
#include <QList>
#include "stream_video_modes.h"
using CallCameraDevices = QList<QPair<QString, QByteArray>>;
// Только аппаратные режимы камеры, пригодные для звонка; опрос выполняется в процессе захвата.
struct CallCameraDevice {
QString name;
QByteArray id;
QList<StreamVideoMode> modes;
};
using CallCameraDevices = QList<CallCameraDevice>;
constexpr qint64 CallCameraMaxFrameAgeUs = 250000;
// GUI получает только собственную CPU-память. Процесс захвата завершается асинхронно.
+7 -1
View File
@@ -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<void(QDataStream&)>& write = {}) {
QByteArray result;
+34 -10
View File
@@ -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<QMediaCaptureSession>(); sink = std::make_unique<QVideoSink>();
camera = std::make_unique<QCamera>(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"));
});