diff --git a/AGENTS.md b/AGENTS.md index ad82f1238..97e4468bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index 2b357de59..de03470c7 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -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`. Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы. diff --git a/doc/stream.txt b/doc/stream.txt index 5434c116f..9f98531c2 100644 --- a/doc/stream.txt +++ b/doc/stream.txt @@ -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 на источник. diff --git a/tools/chatgui/src/stream_publish_window.h b/tools/chatgui/src/stream_publish_window.h index c8d5afac2..2622ca98c 100644 --- a/tools/chatgui/src/stream_publish_window.h +++ b/tools/chatgui/src/stream_publish_window.h @@ -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 отдельно. diff --git a/tools/chatgui/src/video_capture.h b/tools/chatgui/src/video_capture.h index 0378eb71f..ab6ba8e4c 100644 --- a/tools/chatgui/src/video_capture.h +++ b/tools/chatgui/src/video_capture.h @@ -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); diff --git a/tools/chatgui/src/video_capture_buffer.h b/tools/chatgui/src/video_capture_buffer.h index e46fb8fd1..bc8753881 100644 --- a/tools/chatgui/src/video_capture_buffer.h +++ b/tools/chatgui/src/video_capture_buffer.h @@ -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(MapViewOfFile(m_mapping, FILE_MAP_WRITE, 0, 0, m_capacity)); diff --git a/tools/chatgui/src/video_capture_protocol.h b/tools/chatgui/src/video_capture_protocol.h index 8d7f8345c..5ab3001fc 100644 --- a/tools/chatgui/src/video_capture_protocol.h +++ b/tools/chatgui/src/video_capture_protocol.h @@ -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 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. */ diff --git a/tools/chatgui/src/video_capture_source.h b/tools/chatgui/src/video_capture_source.h index 792c965a4..ffdc337c2 100644 --- a/tools/chatgui/src/video_capture_source.h +++ b/tools/chatgui/src/video_capture_source.h @@ -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. */