Document asynchronous desktop stream publication and settings

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent cb95cf7d27
commit fe7cd73646
2 files changed
+94 -82

No files matched your search

+63 -57
View File
@@ -1,6 +1,10 @@
/* Desktop publisher: GUI управляет источниками и AudioDevice, workers кодируют видео/Opus.
* Окно не владеет трансляцией. Stop закрывает callbacks; GUI ждёт workers асинхронно, затем отправляет FIN.
* Captured video/preview хранят только последний кадр; публикация использует общий C actor. */
/* Desktop publisher принадлежит MainWindow и переживает закрытие окна управления.
* Публичные методы — GUI-поток: start открывает actor/capture, но первый KEY и Live приходят асинхронно.
* reconfigure публикует желаемую конфигурацию; ошибка видео откатывает к последней рабочей, повторная ошибка останавливает.
* Audio callback только копирует PCM; Opus/video кодируются в отдельных workers, QVideoFrame преобразует долгоживущий image worker.
* Capture/preview хранят последний кадр, данные уходят через общий C publisher actor.
* stop закрывает capture, оставляя GUI доступным; отдельный join ждёт завершения workers/TLS, затем finishStop отправляет FIN.
* active остаётся true до окончания actor; новый start разрешён после active=false. Деструктор ждёт также image worker, обрабатывая Qt events. */
#pragma once
#include <QObject>
#include <QTimer>
@@ -17,25 +21,25 @@
#include "../../../src/stream/stream_publisher.h"
struct StreamVideoSource {
enum Kind { Camera, Screen, Window };
Kind kind = Camera;
QString name;
QByteArray id;
QVariant window;
QSize size;
QList<StreamVideoMode> modes;
enum Kind { Camera, Screen, Window }; // Window поддерживается openSource, но sources() перечисляет только camera/screen.
Kind kind = Camera; // Backend захвата.
QString name; // Отображаемое имя; не часть идентичности источника.
QByteArray id; // Camera device-id, дескриптор screen либо "portal" для системного выбора Wayland.
QVariant window; // QCapturableWindow для Kind::Window; пустой у camera/screen.
QSize size; // Физический размер screen; может быть неизвестен до первого кадра portal.
QList<StreamVideoMode> modes; // Режимы камеры из объявленных возможностей; screen-режимы строятся отдельно.
QPointer<QScreen> screen; // Живой монитор; удаление устройства не должно переключать захват на его одноимённого соседа.
bool sameSource(const StreamVideoSource& other) const {
bool sameSource(const StreamVideoSource& other) const { // Сравнивает backend/handles, без имени, разрешения и списка режимов.
return kind == other.kind && id == other.id && window == other.window && screen == other.screen;
}
};
struct StreamPublishConfig {
StreamVideoSource source;
LiveVideoConfig video;
QString microphone;
bool audio = true;
bool showCursor = true;
bool nativeSize = false, automaticBitrate = false;
StreamVideoSource source; // Выбранный capture; camera-режим должен поддерживать video.width/height/fps.
LiveVideoConfig video; // Параметры encoder; поддерживаемый codec, fps 1..60, размеры в допустимом диапазоне encoder.
QString microphone; // Стабильный id устройства SoundManager; пустой — default. Меняется только до start.
bool audio = true; // Открыть микрофон и stereo Opus 48кГц/128кбит/с; во время публикации меняется только mute.
bool showCursor = true; // Учитывается поддерживаемым screen backend; portal разрешает выбор только до открытия сессии.
bool nativeSize = false, automaticBitrate = false; // Screen: исходные размеры до 4096 и пересчёт bitrate при уточнении размера.
bool adaptiveQp = false; // При перерасходе битрейта плавно повышать потолок выше video.maxQp, затем возвращать.
};
class StreamPublishEngine : public QObject {
@@ -43,51 +47,53 @@ class StreamPublishEngine : public QObject {
public:
explicit StreamPublishEngine(QObject* parent = nullptr);
~StreamPublishEngine() override;
bool start(uint64_t group, const StreamPublishConfig& config);
bool reconfigure(const StreamPublishConfig& config);
void stop();
bool active() const;
bool running() const;
uint64_t group() const;
StreamPublishConfig config() const;
bool start(uint64_t group, const StreamPublishConfig& config); // true — запуск принят; результат/ошибки ещё асинхронны. false может начать stop.
bool reconfigure(const StreamPublishConfig& config); // true — принято, не подтверждение KEY; микрофон/audio и cursor сессии portal менять нельзя.
void stop(); // Идемпотентный запрос остановки; освобождение workers/microphone и доотправка actor завершаются через tick.
bool active() const; // Actor-id ещё принадлежит engine, включая запуск и остановку/дренирование.
bool running() const; // Capture/encoder ещё работают либо ждут выбора portal/разрешения CPU; stop сразу даёт false.
uint64_t group() const; // Группа текущего actor, 0 после его окончательного завершения.
StreamPublishConfig config() const; // Копия wanted под lock; может ещё ожидать применения/отката.
uint64_t videoBitrate() const; // Фактический выход video encoder за последний интервал около секунды, бит/с; читать в GUI-потоке.
int videoMaxQp() const; // Текущий потолок QP с учётом адаптации; -1 — настройки encoder.
QVariant previewFrame() const;
QList<int> codecs() const;
static QList<StreamVideoSource> sources();
static bool supported();
static bool cursorSupported(const StreamVideoSource& source);
void setMuted(bool muted);
bool muted() const;
uint64_t softwareEncodingRequest() const;
QVariant previewFrame() const; // Shared QVideoFrame последнего capture, пустой/невалидный без кадра; сетевое кодирование не требуется.
QList<int> codecs() const; // Кодеки LiveVideoEncoder, найденные при создании; пусто без поддержки capture.
static QList<StreamVideoSource> sources(); // Текущие камеры и screen: Wayland даёт один portal, остальные платформы — QScreen handles.
static bool supported(); // Возможности сборки HAVE_STREAM_CAPTURE; не гарантия наличия устройства или рабочего encoder.
static bool cursorSupported(const StreamVideoSource& source); // Наличие cursor backend для выбранного screen/платформы.
void setMuted(bool muted); // Только running+audio: меняет флаг actor и обнуляет исходящий PCM; ошибку смены трактует как отказ публикации.
bool muted() const; // Текущее mute; сбрасывается при start.
uint64_t softwareEncodingRequest() const; // Поколение ожидающего запроса CPU либо 0.
// Ответ только текущему запросу; устаревший игнорируется, отказ вызывает rollback/stop.
void answerSoftwareEncoding(uint64_t generation, bool allow);
signals:
void codecsChanged();
void sourcesChanged();
void stateChanged(const QString& state);
void configurationChanged();
void previewVideoChanged(const QVariant& frame);
void telemetryChanged(const stream_publisher_status& status);
void errorOccurred(const QString& error);
void audioChannelsChanged(int channels);
void codecsChanged(); // Сигнал перечня codec; текущая реализация определяет список один раз в конструкторе.
void sourcesChanged(); // Камеры/мониторы изменились либо уточнён размер захваченного screen; GUI перечитывает sources/config.
void stateChanged(const QString& state); // Пользовательский текст запуска, Live, применения, остановки или ожидания CPU/portal.
void configurationChanged(); // Настройки подтверждены либо изменилось состояние start/stop/mute; перечитать config/active/running.
void previewVideoChanged(const QVariant& frame); // GUI публикует новый QVideoFrame до ~30fps; пустой QVariant очищает preview при closeSource.
void telemetryChanged(const stream_publisher_status& status); // Снимок actor, примерно каждые 100мс вне ожидания workers.
void errorOccurred(const QString& error); // Ошибка может привести к rollback либо stop; сама не означает active=false.
void audioChannelsChanged(int channels); // Фактический mono/stereo вход; mono дублируется в stereo-транспорт.
// HW не открылся; требуется явный ответ для CPU.
void softwareEncodingRequested(uint64_t generation, const QString& codec, const QString& details);
private:
struct Impl;
std::unique_ptr<Impl> m;
AudioCapture m_capture;
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Exclusive};
QTimer m_timer;
QTimer m_previewTimer;
bool openSource(const StreamPublishConfig& config);
void closeSource();
bool openMicrophone();
void sourceFailed(uint64_t generation, const QString& error);
void publicationFailed(uint64_t id, const QString& error);
void tick();
bool finishStop();
bool finishImageStop();
void imageLoop();
void videoLoop(StreamPublishConfig initial);
void audioLoop();
void captureAudio(const AudioCapture::Frame& frame);
std::unique_ptr<Impl> m; // Workers, Qt capture backend, wanted/good generations, очередь PCM и последний video/preview.
AudioCapture m_capture; // Микрофон 48кГц, callback копирует PCM без DSP/codec.
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Exclusive}; // Удерживается до закрытия устройства и завершения workers.
QTimer m_timer; // GUI 100мс: finishStop, состояние actor, telemetry, bitrate и таймауты capture.
QTimer m_previewTimer; // GUI 33мс: отправляет только изменившийся previewFrame.
bool openSource(const StreamPublishConfig& config); // Пересоздаёт capture backend в GUI, true — принят запуск; ошибки возможны позже.
void closeSource(); // Меняет sourceGeneration, очищает кадры, отключает callbacks и закрывает backend.
bool openMicrophone(); // true — открыт вход; нормализует каналы и подключает captureAudio.
void sourceFailed(uint64_t generation, const QString& error); // Только текущее поколение: однократный rollback к good либо stop.
void publicationFailed(uint64_t id, const QString& error); // Ошибка всей текущей публикации, без video rollback: errorOccurred и stop.
void tick(); // GUI-проверка состояния actor/остановки, capture/audio timeout и статистики.
bool finishStop(); // true — workers join и закрытие микрофона завершены, FIN уже отправлен; actor ещё может дренироваться.
bool finishImageStop(); // Неблокирующий wait(0) image QThread при уничтожении; true — native TLS уже освобождён.
void imageLoop(); // Долгоживущий QThread: последний QVideoFrame → QImage, rotation/mirror и cursor Windows.
void videoLoop(StreamPublishConfig initial); // Video worker: encoder/revision, HW→CPU consent, KEY, bitrate/QP и feed actor.
void audioLoop(); // Audio worker: 20мс stereo, compressor/Opus, пропуск устаревших blocks, feed actor.
void captureAudio(const AudioCapture::Frame& frame); // Hardware callback: до 960 frames на элемент ограниченной очереди, без allocations/codec.
};
+31 -25
View File
@@ -1,4 +1,8 @@
/* Управление publisher: окно можно закрыть и открыть повторно, engine живёт у MainWindow. */
/* GUI управления StreamPublishEngine: окно не владеет engine, он должен жить дольше окна (в MainWindow это отдельный объект).
* Закрытие/Esc скрывает окно и сообщает closed, публикация продолжает работать. Повторное открытие MainWindow создаёт новое окно.
* settings — компактный JSON пользовательского выбора; здесь нет БД, сохранение выполняет владелец по settingsChanged.
* Пока engine активен, UI синхронизируется с wanted config; start/stop/reconfigure подтверждаются асинхронно.
* Отсутствие HW encoder открывает немодальный диалог согласия на CPU для поколения запроса; отказ/удаление диалога отвечает false. */
#pragma once
#include <QWidget>
#include <QJsonObject>
@@ -14,34 +18,36 @@ class StreamBufferIndicator;
class StreamPublishWindow : public QWidget {
Q_OBJECT
public:
// engine обязателен; group используется при start, действующий actor остаётся в своей группе. settings восстанавливаются только до start.
StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent = nullptr, const QString& settings = {});
signals:
void settingsChanged(const QString& settings);
void closed(); // Возвращает фокус окну, из которого открыто управление; публикация продолжает работать.
void settingsChanged(const QString& settings); // Новый компактный JSON, только при изменении выбора; runtime handles не сохраняются.
void closed(); // Принят closeEvent; MainWindow использует сигнал для возврата фокуса в grid, без остановки публикации.
protected:
void closeEvent(QCloseEvent* event) override;
private:
StreamPublishEngine* m_engine;
uint64_t m_group;
QList<StreamVideoSource> m_sources;
QList<StreamVideoMode> m_modes;
QComboBox *m_source, *m_microphone, *m_mode, *m_codec, *m_bitrate;
QCheckBox *m_audio, *m_mute, *m_cursor;
QSpinBox* m_maxQp;
QCheckBox* m_adaptiveQp;
QLabel *m_status, *m_stats, *m_inputChannels;
QPushButton* m_start;
StreamBufferIndicator* m_buffer;
QButtonGroup* m_rotation;
QWidget* m_preview;
bool m_updating = false;
QJsonObject m_settings;
StreamPublishConfig selection() const;
void refreshSources();
StreamPublishEngine* m_engine; // Заимствован; QWidget-дерево владеет только дочерними controls.
uint64_t m_group; // Группа для следующего запуска из этого окна.
QList<StreamVideoSource> m_sources; // Текущий список camera/screen, индексы совпадают с m_source.
QList<StreamVideoMode> m_modes; // Режимы выбранного источника, индексы совпадают с m_mode.
QComboBox *m_source, *m_microphone, *m_mode, *m_codec, *m_bitrate; // Выбор source/device/mode/codec и ручного либо auto bitrate.
QCheckBox *m_audio, *m_mute, *m_cursor; // Audio/device фиксируются на start; mute меняется отдельно, cursor portal — только до start.
QSpinBox* m_maxQp; // UI 0 означает Automatic → config.maxQp=-1; явные значения 1..51 только H.264/H.265.
QCheckBox* m_adaptiveQp; // Доступен при явном maxQp; engine проверяет поддержку live QP у encoder.
QLabel *m_status, *m_stats, *m_inputChannels; // Статус/ошибка, bitrate/QP/очереди/ACK и фактические входные каналы.
QPushButton* m_start; // Start/Stop; отключена во время завершения actor либо при отсутствии доступных source/mode/codec.
StreamBufferIndicator* m_buffer; // Диапазоны получателей по истории source: oldest → live; tooltip содержит peer/subflows.
QButtonGroup* m_rotation; // Варианты 0/90/270°, задают метаданные rotation для зрителя.
QWidget* m_preview; // QVideoWidget при HAVE_STREAM_CAPTURE, иначе текст о недоступности capture.
bool m_updating = false; // Защита от применения/сохранения промежуточного состояния при перестроении controls.
QJsonObject m_settings; // Последний сохранённый выбор; source id в base64, окно по имени, режим/codec/audio/cursor/QP отдельно.
StreamPublishConfig selection() const; // Снимок controls: auto bitrate рассчитан, screen nativeSize и допустимость QP учтены.
void refreshSources(); // Сохраняет выбранный handle; пропавший active-source не подменяется другим, до start возможен первый доступный.
// Режимы camera/screen; выбирает близкий previous либо native screen.
void refreshModes(const StreamPublishConfig& previous, bool maximum = false);
void refreshCodecs();
void synchronize();
void apply();
void restoreSettings();
void saveSettings();
void refreshCodecs(); // Перечитывает engine.codecs(), сохраняет текущий/saved codec, недоступный заменяет первым.
void synchronize(); // Читает engine config/state без apply; настраивает controls и ограничения изменения audio/cursor/QP.
void apply(); // Для выбранного режима: reconfigure только active-engine, затем сохранение выбора; не во время m_updating.
void restoreSettings(); // До start восстанавливает доступные значения JSON; некорректные/отсутствующие диагностируются и заменяются.
void saveSettings(); // Сериализует пользовательские настройки без runtime handles/вычисленного auto bitrate и эмитит лишь изменение JSON.
};