100 lines
11 KiB
C++
100 lines
11 KiB
C++
/* 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>
|
||
#include <QVariant>
|
||
#include <QImage>
|
||
#include <QList>
|
||
#include <QPointer>
|
||
#include <QScreen>
|
||
#include <memory>
|
||
#include "live_video_encoder.h"
|
||
#include "stream_video_modes.h"
|
||
#include "audio_capture.h"
|
||
#include "audio_capture_lease.h"
|
||
#include "../../../src/stream/stream_publisher.h"
|
||
|
||
struct StreamVideoSource {
|
||
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 { // Сравнивает backend/handles, без имени, разрешения и списка режимов.
|
||
return kind == other.kind && id == other.id && window == other.window && screen == other.screen;
|
||
}
|
||
};
|
||
struct StreamPublishConfig {
|
||
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 {
|
||
Q_OBJECT
|
||
public:
|
||
explicit StreamPublishEngine(QObject* parent = nullptr);
|
||
~StreamPublishEngine() override;
|
||
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; // 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(); // Сигнал перечня 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; // 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.
|
||
};
|