Document asynchronous desktop stream publication and settings
This commit is contained in:
1 parent
cb95cf7d27
commit
fe7cd73646
2 files changed
+94
-82
No files matched your search
@@ -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.
|
||||
};
|
||||
@@ -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.
|
||||
};
|
||||
Reference in new issue
Block a user