Document isolated stream capture shutdown and diagnostics

This commit is contained in:
evgeny committed 2026-11-01 20:18:34 +01:00
1 parent 8c2289b188
commit caf4ab406c
8 files changed
+41 -29

No files matched your search

+5 -5
View File
@@ -827,11 +827,11 @@ cmake .. && make -j4
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
| `video_capture.h/cpp` | Асинхронный capture-процесс, каталог источников, watchdog и независимое от GUI закрытие |
| `video_capture_worker.cpp` | Камера/экран/окно, преобразование в CPU RGB32 и диагностика этапов аппаратного закрытия |
| `video_capture_source.h` | Контракт backend внутри worker и точка входа для тестовых источников |
| `video_capture_protocol.h` | Поколения, FRAME/ACK, этапы/heartbeat и ограничения IPC видеозахвата |
| `video_capture_buffer.h` | Однокадровый shared-memory буфер согласованной ёмкости до 4096×4096 RGB32 |
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
+13 -8
View File
@@ -406,19 +406,24 @@ stats timer и stream handle освобождаются до контекста
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
Общий `VideoCapture` запускает отдельный процесс для камеры/экрана/окна и преобразования кадров.
Аппаратные Qt-объекты и все `QVideoFrame` остаются в дочернем процессе. GUI копирует RGB32 из
однокадрового shared memory до FRAME/ACK и передаёт собственный CPU `QImage` в preview и video worker.
Размеры Call ограничены Full HD, Stream — 4096 по каждой стороне. Поздние кадры отсекаются поколением.
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
Stop запрещает producers и сразу закрывает admission C publisher: принятые access units дренируются,
FIN с drain 2s не ждёт capture или encoder. Состояние engine сохраняется до полного выхода video/audio
threads, закрытия микрофона и capture-процесса. Новый start ждёт также FIN drain.
Независимый QObject-supervisor переживает владельца GUI; только QProcess::finished подтверждает
освобождение native/TLS capture. Deadline выхода 1.5s проверяется каждые 250ms и не продлевается
этапами/heartbeat. При таймауте supervisor пишет PID, поколение, последний этап, время и завершает процесс.
Таймаут активности камеры учитывает свежие кадры; неподвижный экран остаётся жив по heartbeat.
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
чтобы encoder thread cleanup мог обратиться к GUI; capture-supervisor освобождается независимо.
Start и внешние сигналы engine в это время запрещены; деструктор живого QProcess в GUI не вызывается.
Workers никогда не держат borrowed instance/handle.
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
+6 -1
View File
@@ -1093,7 +1093,12 @@ Desktop:
- transport/stream_bridge владеет независимым stream_publisher; shared C actor выполняет uAV2/RS,
admission, KEY recovery, FIN и TX snapshot в uasync. Очереди/pressure/drain совпадают с Android.
UI показывает video queue, суммарный inflight, send buffer и ACK_1s каждые 100ms.
Stop закрывает источники и audio callback, join обоих workers, затем FIN и ограниченный drain 2s.
Захват и преобразование кадров выполняет общий VideoCapture в отдельном процессе; аппаратные
QVideoFrame никогда не передаются GUI. Preview и encoder получают собственные CPU QImage через FRAME/ACK.
Stop закрывает admission publisher и сразу начинает FIN/drain 2s независимо от физического capture close.
Новый start ждёт workers, микрофон, фактический выход capture-процесса и завершение drain.
Capture-supervisor сохраняет этапы закрытия, PID/поколение/длительность; после deadline 1.5s завершает
зависший процесс. Неподвижный экран проверяется по heartbeat, без требования новых кадров.
Удаление engine происходит до SoundManager и ядра; detach отменяет posts/timers и закрывает publisher
в uasync. Общий publisher использует те же uAV2 и текущий wire v7 на обеих платформах.
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
+1 -1
View File
@@ -41,7 +41,7 @@ private:
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.
QWidget* m_preview; // QVideoWidget при HAVE_STREAM_CAPTURE, иначе текст о недоступности capture.
QWidget* m_preview; // VideoRenderWidget с CPU QImage при HAVE_STREAM_CAPTURE, иначе текст о недоступности capture.
bool m_updating = false; // Защита от применения/сохранения промежуточного состояния при перестроении controls.
bool m_audioMuted = false; // Сохранённый mute для следующего запуска; во время трансляции синхронизируется с engine.
QJsonObject m_settings; // Последний сохранённый выбор; source id в base64, окно по имени, режим/codec/audio/cursor/QP отдельно.
+6 -6
View File
@@ -37,10 +37,9 @@ public:
explicit VideoCapture(QObject* parent = nullptr, Usage usage = Usage::Call);
/* Отвязывает GUI-владельца и запрашивает асинхронный Quit; сессия сама дождётся выхода/убьёт зависший worker. */
~VideoCapture() override;
/* enabled включает/выключает захват; device — точный id из devicesChanged, пустой id не выбирает камеру автоматически.
* width/height должны пройти VideoCaptureProtocol::validSize, fps=1..60 — предел выдачи, не команда драйверу.
* Параметры проверяются и при enabled=false; повтор тех же параметров без ошибки ничего не меняет.
* Смена конфигурации асинхронна; после failed явный configure(true, ...) допускает перезапуск процесса. */
/* Call принимает камеры Full HD, Stream — камеры/экраны/окна до 4096. FPS 1..60 ограничивает выдачу.
* Параметры проверяются и при enabled=false; повтор конфигурации без ошибки ничего не меняет.
* После failed enabled=true допускает перезапуск; во время stop конфигурация отклоняется с WARN. */
void configure(const VideoCaptureConfig& config);
void stop(); // Запросить выход процесса; deadline 1.5 с не продлевается сообщениями worker. Повтор безопасен.
bool closing() const; // true до подтверждённого выхода процесса после stop; GUI никогда не ждёт его синхронно.
@@ -53,7 +52,8 @@ signals:
* уменьшение опережения исходных PTS. Ack worker отправлен до сигнала, mapping больше не удерживается. */
void frameReady(const QImage& image, qint64 pts, qint64 correction);
/* Ошибка запуска/IPC/драйвера/параметров или watchdog: процесс останавливается, захват отключается.
* Watchdog: отсутствие прогресса/свежих кадров около 5 с, остановка — около 1.5 с, проверка каждые 250 мс. */
* Watchdog: отсутствие прогресса или кадров камеры около 5 с; неподвижному экрану достаточно heartbeat.
* stop контролирует фактический выход процесса с deadline 1.5 с, проверка каждые 250 мс. */
void failed(const QString& error);
void stopped(); // Процесс фактически вышел, включая уничтожение Qt/TLS; перед этим повторный start запрещён.
private:
@@ -61,7 +61,7 @@ private:
Session* m; // Независимый QObject с QPointer на владельца; после shutdown живёт до завершения worker/event loop.
};
/* Entry point дочернего процесса: argc=3, argv[2] — endpoint, создаёт QGuiApplication и запускает worker event loop.
/* Entry point дочернего процесса: argc=4, argv[2] — endpoint, argv[3] — call/stream; создаёт QGuiApplication.
* Вызывается до обычной инициализации приложения, конфига, БД и single-instance lock.
* Возвращает код event loop; без HAVE_STREAM_CAPTURE или с неверным argc — 2. */
int video_capture_worker_main(int argc, char** argv);
+2 -2
View File
@@ -1,4 +1,4 @@
/* Однокадровый mapping VideoCaptureProtocol::FrameBytes: GUI создаёт, capture worker подключается по name().
/* Однокадровый mapping согласованной ёмкости до MaxFrameBytes: GUI создаёт, capture worker подключается по name().
* Объект единолично владеет локальным mapping/handle, не копируется; вызовы не синхронизированы.
* Доступ к общим пикселям согласуется через Frame/Ack, а не через этот класс. close не ждёт другой процесс. */
#pragma once
@@ -23,7 +23,7 @@ class VideoCaptureBuffer {
#endif
/* Запоминает причину, освобождает локальные ресурсы и возвращает false. */
bool fail(const QString& error) { m_error = error; close(); return false; }
/* Отображает FrameBytes текущего handle/файла; false освобождает ресурсы через fail. */
/* Отображает согласованную ёмкость handle/файла; false освобождает ресурсы через fail. */
bool map() {
#ifdef Q_OS_WIN
m_data = static_cast<uchar*>(MapViewOfFile(m_mapping, FILE_MAP_WRITE, 0, 0, m_capacity));
+5 -3
View File
@@ -1,4 +1,4 @@
/* IPC GUI <-> camera worker: QLocalSocket передаёт QDataStream Qt_5_15, пиксели лежат в VideoCaptureBuffer.
/* IPC GUI <-> video capture worker: QLocalSocket передаёт QDataStream Qt_5_15, пиксели лежат в VideoCaptureBuffer.
* Пакет: quint32 длины тела, quint8 Type, quint64 поколения конфигурации, затем поля конкретного типа.
* Worker пишет один RGB32-кадр и посылает Frame; до Ack его пиксели не меняются, в том числе при Configure.
* Получатель копирует/отбрасывает кадр и подтверждает его поколение; shared mutex между процессами нет. */
@@ -9,8 +9,10 @@
#include <functional>
namespace VideoCaptureProtocol {
/* GUI -> worker: Configure(bool,QByteArray device,qint32 w/h/fps), Quit(), Ack(), Buffer(QString name).
* Worker -> GUI: Devices(count,[name,id,modes(w,h,fps)]), Frame(qint64 pts/correction,qint32 w/h), Stage/Error(QString).
/* GUI -> worker: Configure(bool,QByteArray device,qint32 w/h/fps,quint8 kind,bool cursor), Quit(), Ack(),
* Buffer(QString name,quint32 capacity), Refresh(). Worker -> GUI:
* Devices(count,[name,id,count_modes,quint8 kind,QSize,modes(w,h,fps)]), Frame(qint64 pts/correction,qint32 w/h),
* Stage/Error(QString), Heartbeat(). Usage call/stream выбирается аргументом запуска.
* Buffer/Devices используют поколение 0; остальные — поколение конфигурации/подтверждаемого кадра. */
enum Type : quint8 { Configure = 1, Quit, Ack, Devices, Frame, Stage, Error, Buffer, Heartbeat, Refresh };
constexpr quint32 FrameBytes = 1920 * 1080 * 4; /* Буфер Call; Stream согласует буфер до MaxFrameBytes. */
+3 -3
View File
@@ -1,4 +1,4 @@
/* Backend камеры внутри изолированного worker-процесса; интерфейс для Qt и тестовых источников.
/* Backend видео внутри изолированного worker-процесса; интерфейс для Qt и тестовых источников.
* Управление/опрос идут в потоке Qt worker event loop и могут блокироваться в драйвере: watchdog GUI завершит процесс.
* Сигнал frame может приходить из потока backend; worker забирает последний кадр и конвертирует вне callback. */
#pragma once
@@ -11,11 +11,11 @@
class VideoCaptureSource : public QObject {
Q_OBJECT
public:
/* Возвращает текущий полный список с аппаратными размерами/FPS передачи, допустимыми для звонка. */
/* Список: stream=false ограничивает камеры до Full HD; true добавляет камеры 4096, экраны и окна. */
virtual VideoCaptureDevices devices(bool stream) = 0;
/* Запрашивает точный device/размер и предел FPS выдачи; worker вызывает stop перед каждым start.
* true — запуск принят, готовность наступает с frame; отказ/асинхронная ошибка сообщаются также сигналом error.
* Сам return false не создаёт IPC-ошибку. Qt backend выбирает подходящий формат без масштабирования кадров. */
* Сам return false не создаёт IPC-ошибку. Камера выбирает аппаратный формат; экран вписывается в 4096 перед IPC. */
virtual bool start(const VideoCaptureConfig& config) = 0;
virtual bool prepareImage(QImage&) { return true; } // Наложение курсора до IPC; только worker event loop.
/* Освобождает захват; должен допускать вызов без успешного start и повтор при уничтожении worker. */