Clarify desktop video modes and encoder output contracts

This commit is contained in:
evgeny committed 2026-10-27 13:43:00 +01:00
1 parent b1510ab437
commit e9391c9790
2 files changed
+65 -26

No files matched your search

+46 -18
View File
@@ -1,9 +1,10 @@
/* FFmpeg live encoder: один worker владеет контекстом, конвертером и decoder configuration.
* open/encode/close последовательны; каждый KEY содержит uAV2 envelope. Пакеты имеют capture PTS.
* Backend ограничивает перебор: HardwareOnly никогда не открывает software encoder.
* Профили звонка проверяются по bitstream; допустим задержанный выход GPU без B-кадров.
* encode выдаёт все готовые кадры сразу. До восьми входных PTS сохраняются до выхода;
* QSV использует 90kHz внутри codec, наружу возвращается точный исходный capture PTS. */
/* FFmpeg live encoder для звонков/трансляций: один worker последовательно вызывает все методы экземпляра.
* open синхронно пробует реальный KEY и принудительный KEY (если backend умеет), затем открывает чистый контекст.
* Профили звонка дополнительно проверяются по bitstream/декодированию, включая уровень и отсутствие B-кадров.
* encode добавляет весь готовый выход в список; задержка допустима. Для профилей звонка сохраняет не более восьми
* входных PTS и проверяет порядок выхода; QSV использует 90 kHz внутри codec, наружу возвращается capture PTS.
* KEY data: config_length:4BE + decoder configuration + access unit; остальные — access unit (без заголовка uAV2-блока).
* close/деструктор отбрасывают задержанный выход; для его получения сначала вызвать encode с пустым image и finish=true. */
#pragma once
#include <QByteArray>
#include <QImage>
@@ -12,34 +13,61 @@
#include <memory>
#include <cstdint>
/* Automatic — обычная трансляция; два явных профиля включают строгий bitstream-контракт звонка. */
enum class LiveVideoProfile { Automatic, H264ConstrainedBaseline, H265Main };
struct LiveVideoConfig {
int width = 1280, height = 720, fps = 30, bitrate = 2500000, codec = 1, rotation = 0;
LiveVideoProfile profile = LiveVideoProfile::Automatic;
int level = 0, references = 0, vbvBits = 0; // level: AVC level_idc / HEVC general_level_idc; 0 — auto.
int maxQp = -1; // Потолок QP для H.264/H.265: 0..51, -1 — настройки encoder. Профили звонка не используют.
QString backend; // Пусто — проба доступных; непусто — только уже проверенный encoder.
int width = 1280, height = 720, /* Чётные размеры кодированного кадра, каждая сторона 16..4096. */
fps = 30, /* Номинальная частота 1..60; encode сам не ограничивает подачу кадров. */
bitrate = 2500000, /* Целевой битрейт 64000..16000000 бит/с. */
codec = 1, /* STREAM_MEDIA_H264/H265/VP9/AV1/H266, значения 1..5. */
rotation = 0; /* Метаданные проверки профиля звонка; пиксели здесь не поворачиваются. */
LiveVideoProfile profile = LiveVideoProfile::Automatic; /* Совместим с codec; явные профили только для H.264/H.265. */
int level = 0, references = 0, vbvBits = 0; /* AVC/HEVC level_idc, число refs, VBV в битах; для звонка: level>0, refs=1, VBV>0.
* В Automatic: level определяет encoder, refs=0 — его выбор, VBV=0 — bitrate/4. */
int maxQp = -1; /* Потолок QP 0..51 либо -1 (выбор encoder); только H.264/H.265 Automatic. */
QString backend; /* Пусто — перебор доступных; иначе точное имя FFmpeg encoder с учётом Backend. */
/* Сравнивает все поля, включая выбранный backend. */
bool operator==(const LiveVideoConfig&) const = default;
};
struct LiveEncodedFrame { QByteArray data; uint64_t pts = 0; bool key = false; };
struct LiveEncodedFrame {
QByteArray data; /* Владеет KEY payload или обычным access unit, максимум STREAM_MEDIA_MAX_FRAME. */
uint64_t pts = 0; /* Исходный capture PTS в микросекундах, <=INT64_MAX. */
bool key = false; /* Фактический KEY по выходному пакету encoder, не копия запроса key. */
};
class LiveVideoEncoder {
public:
/* Ограничивает список имён encoder: Automatic — GPU/CPU, HardwareOnly — только GPU, SoftwareOnly — только CPU. */
enum class Backend { Automatic, HardwareOnly, SoftwareOnly };
LiveVideoEncoder();
~LiveVideoEncoder();
/* Сначала close прежнего контекста, затем валидация config и пробы encoder. true — готовый чистый контекст.
* false — смотреть error; продолжать encode после неудачного open нельзя. Без FFmpeg всегда false. */
bool open(const LiveVideoConfig& config, Backend backend = Backend::Automatic);
bool setBitrate(int bitrate); // true — изменение на следующем кадре; false — нужна смена видеоконтекста
bool canSetMaxQp() const; // Поддерживается изменение потолка без пересоздания encoder.
bool setMaxQp(int qp); // 0..51, применяется при следующей подаче кадра; false — ошибка/нет поддержки.
/* 64000..16000000 бит/с; true — обновлено для следующего кадра (только libx264 Automatic).
* false — неверный битрейт либо требуется замена контекста; прежний контекст сохраняется. */
bool setBitrate(int bitrate);
/* Динамический QP доступен для H.264/H.265 Automatic: QSV либо AMF с опцией utun_qp_max. */
bool canSetMaxQp() const;
/* Запрашивает потолок 0..51 на следующей подаче кадра; false — ошибка/нет поддержки, причина записывается в error. */
bool setMaxQp(int qp);
/* Освобождает контекст без flush, очищает имя/ошибку/PTS; повтор безопасен. */
void close();
// Пустое image забирает готовый выход без нового input; finish завершает старый поток перед заменой.
/* Масштабирует image с сохранением пропорций/чёрными полями под config, подаёт capture pts<=INT64_MAX и забирает выход.
* key — запрос KEY; backend без принудительных KEY (AMF) после первого кадра использует периодический GOP.
* Пустое image только забирает выход; finish=true требует пустое image и завершает подачу потока (после него close/open).
* output дополняется, не очищается; true допускает пустой выход. При false вход/часть выхода уже могли быть приняты. */
bool encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& output, bool finish = false);
/* Число принятых, но ещё не выданных входов профиля звонка; Automatic не ведёт эту таблицу и возвращает 0. */
unsigned pendingFrames() const;
/* Последняя записанная ошибка; успешный encode/setMaxQp не обязан очищать её. */
QString error() const;
/* Имя FFmpeg encoder текущей/последней пробы; после close пусто, само по себе не означает успешный open. */
QString name() const;
static QList<int> available(Backend backend = Backend::Automatic); // Установленные кодеки, без открытия и кодирования.
/* ID кодеков с зарегистрированным encoder указанного Backend; без открытия/проверки GPU, без FFmpeg — пусто. */
static QList<int> available(Backend backend = Backend::Automatic);
/* Имя протокольного codec 1..5; неизвестный ID возвращает пустую строку. */
static QString codecName(int codec);
private:
struct Impl;
std::unique_ptr<Impl> m;
std::unique_ptr<Impl> m; /* Единственный владелец codec, кадров, конвертера и таблицы ожидаемых PTS. */
};
+19 -8
View File
@@ -1,6 +1,8 @@
/* Режимы трансляции: экран масштабируется пропорционально, камера сохраняет аппаратные размеры.
* Пустой размер экрана означает ожидание первого кадра системного выбора Wayland.
* FPS — предел частоты трансляции; Qt захватывает с максимальной частотой выбранного формата. */
* FPS — предел частоты трансляции; Qt захватывает с максимальной частотой выбранного формата.
* Функции работают только с переданными значениями/списками; сами не опрашивают устройства и не меняют захват.
* Для звонка CallCameraProtocol::validSize дополнительно ограничивает камеру до Full HD. */
#pragma once
#include <QList>
#include <QSize>
@@ -8,12 +10,14 @@
#include <cmath>
struct StreamVideoMode {
QSize size;
int fps = 30;
QSize size; /* Размер выходного кадра; пустой допустим для ещё не выбранного системного экрана. */
int fps = 30; /* Предел выдачи 1..60; конструктор структуры сам его не проверяет. */
/* Совпадают оба поля; используется для дедупликации и сохранения ручного выбора. */
bool operator==(const StreamVideoMode&) const = default;
};
/* Ограничиваем длинную сторону и, при необходимости, короткую; не увеличиваем исходник. */
/* Вписывает source в длинную сторону 4096 и короткую shortSide, не увеличивая исходник, округляет размеры вниз до чётных.
* Возвращает пустой QSize при source/shortSide <16 либо если после уменьшения одна из сторон <16. */
inline QSize stream_video_screen_size(QSize source, int shortSide = 4096) {
if (source.width() < 16 || source.height() < 16 || shortSide < 16) return {};
double scale = std::min({1.0, 4096.0 / std::max(source.width(), source.height()),
@@ -22,7 +26,8 @@ inline QSize stream_video_screen_size(QSize source, int shortSide = 4096) {
return size.width() >= 16 && size.height() >= 16 ? size : QSize();
}
/* Стандартные ступени короткой стороны дают одинаковые пропорции в обеих ориентациях. */
/* Строит максимум и ступени короткой стороны 2160/1440/1080/720/480/360, убирая дубли размеров, с FPS 15/30/60.
* Первый размер максимальный; для пустого source возвращает три режима с пустым размером, для слишком малого — пустой список. */
inline QList<StreamVideoMode> stream_video_screen_modes(QSize source) {
QList<StreamVideoMode> modes;
if (source.isEmpty()) {
@@ -41,7 +46,8 @@ inline QList<StreamVideoMode> stream_video_screen_modes(QSize source) {
}
/* FPS режима — предел передачи, не команда драйверу. Быстрый capture ограничивается до encode.
* 29.97/59.94 соответствуют пределам 30/60; фактический диапазон камеры не округляется. */
* Проверяет точный size, чётные стороны 16..4096, корректный диапазон minimum..maximum и FPS 1..60 <=maximum+0.1.
* FPS может быть ниже minimum, поскольку лишние кадры отбрасываются. 29.97/59.94 допускают пределы 30/60. */
inline bool stream_video_camera_supports(QSize size, double minimum, double maximum, const StreamVideoMode& mode) {
return size == mode.size && size.width() >= 16 && size.height() >= 16 && size.width() <= 4096 && size.height() <= 4096
&& !(size.width() % 2) && !(size.height() % 2) && std::isfinite(minimum) && std::isfinite(maximum)
@@ -49,7 +55,9 @@ inline bool stream_video_camera_supports(QSize size, double minimum, double maxi
&& mode.fps <= maximum + 0.1;
}
/* Объединяем одинаковые режимы разных pixel format, сохраняя границы необычных диапазонов FPS. */
/* Добавляет в modes допустимые FPS из 15/24/25/30/50/60 и округлённых с допуском 0.1 границ minimum/maximum.
* Объединяет дубли разных pixel format; сортирует весь modes по убыванию площади/ширины, затем возрастанию FPS.
* Невалидный аппаратный формат ничего не добавляет; сама функция не ограничивает камеры до Full HD. */
inline void stream_video_add_camera_modes(QList<StreamVideoMode>& modes, QSize size, double minimum, double maximum) {
QList<int> rates{15, 24, 25, 30, 50, 60};
if (std::isfinite(minimum) && minimum >= 1 && minimum <= 60.1) rates.append(int(std::ceil(minimum - 0.1)));
@@ -67,7 +75,10 @@ inline void stream_video_add_camera_modes(QList<StreamVideoMode>& modes, QSize s
});
}
/* При явном выборе экрана берём максимум; обновление списка сохраняет допустимый ручной выбор. */
/* Возвращает индекс режима или -1 для пустого списка, сам список не меняет.
* При !maximum сначала сохраняет точный previous; иначе/при отсутствии ищет FPS previous/30 с размером
* modes.front().size (maximum) либо 1280x720. Затем берёт первый режим 30 FPS, в крайнем случае индекс 0.
* Для выбора максимума вызывающий должен передать список с максимальным размером в начале. */
inline int stream_video_select_mode(const QList<StreamVideoMode>& modes, StreamVideoMode previous, bool maximum) {
if (modes.isEmpty()) return -1;
if (!maximum) {