diff --git a/tools/chatgui/src/stream_portal_capture.h b/tools/chatgui/src/stream_portal_capture.h index f0cf947c..343124e8 100644 --- a/tools/chatgui/src/stream_portal_capture.h +++ b/tools/chatgui/src/stream_portal_capture.h @@ -22,7 +22,8 @@ public: ~StreamPortalCapture() override; // Начать асинхронный выбор: showCursor=true требует Embedded=2, false — Hidden=1; fallback режима отсутствует. // На служебные этапы отведено 30с, на первый кадр 8с; ожидание пользователя на Start не ограничено. - // Успех возвращается через frameReady; отказ/отмена выбора/повторный start — через errorOccurred. + // Успех — frameReady; отказ/отмена выбора — errorOccurred. Повтор активного start завершает захват ошибкой; + // после Failed start ничего не запускает и повторно не сообщает ошибку. void start(bool showCursor); signals: // Скопированный QVideoFrame; ссылка заимствована на время сигнала, для сохранения скопировать объект. diff --git a/tools/chatgui/transport/stream_bridge.h b/tools/chatgui/transport/stream_bridge.h index cb8b681a..4a316077 100644 --- a/tools/chatgui/transport/stream_bridge.h +++ b/tools/chatgui/transport/stream_bridge.h @@ -2,7 +2,8 @@ * Один общий publisher и независимые viewers; команды содержат IDs, не borrowed pointers ядра. * Плеер запрашивает ограниченный батч; каждый трек имеет свой бюджет, новые данные будят worker. * Watch до attach запоминает выбранную группу. Каталог идёт через CHAT_EVT_STREAM_SOURCES, - * media pull читает ближайшие кадры из общего RS-кольца; копия нужна только для передачи между потоками. */ + * media pull читает ближайшие кадры из общего RS-кольца; QByteArray владеет копией payload. + * notify вызывается под внутренними mutex: только будить worker, не входить повторно в bridge/API ядра. */ #pragma once #include @@ -16,35 +17,63 @@ struct UTUN_INSTANCE; enum class StreamRecordKind { Video, Audio, Gap, Opened, End, Closed, VideoEnd, AudioEnd }; struct StreamRecord { - uint64_t id = 0, pts = 0; - StreamRecordKind kind = StreamRecordKind::Gap; - stream_media_format format{}; - int flags = 0, reason = 0; - QByteArray data; + uint64_t id = 0, pts = 0; // ID viewer и PTS этого источника в микросекундах; PTS значим для media. + StreamRecordKind kind = StreamRecordKind::Gap; // Media либо событие открытия/разрыва/завершения трека. + stream_media_format format{}; // Формат VIDEO/AUDIO; для управляющих событий нули. + int flags = 0, reason = 0; // VIDEO: STREAM_MEDIA_FLAG_KEY; CLOSED: stream_close_reason. + QByteArray data; // Owned access unit/Opus; у управляющих событий пусто, живёт независимо от viewer/ядра. }; +/* Создать общий publisher/viewers для inst с работающим stream-сервисом; uasync, вне callbacks. + * true — managers готовы; false — неверный inst, повторный attach либо отказ создания, с логом нижнего слоя. + * Сохранённые watches применяются к новому ядру; отдельный отказ watch логируется и не отменяет attach. */ bool gui_stream_attach(UTUN_INSTANCE* inst); +/* Только совпавший inst: отменить pending watch, снять watches, уничтожить managers; NULL/старое ядро — no-op. + * uasync вне callbacks до destroy inst; mutex ждёт внешних callers. Выбор watches сохраняется для нового attach. */ void gui_stream_detach(UTUN_INSTANCE* inst); -/* owner=0 — выбранный чат; окно многопросмотра держит свой ненулевой owner. group=0 снимает только его. */ +/* owner=0 — выбранный чат; окно многопросмотра держит свой ненулевой owner. group=0 снимает только его. + * Последнее значение каждого owner сохраняется до attach/через detach; работающий bridge объединяет изменения в post. + * true — выбор принят, не подтверждение доступности каталога. false — не выделен post, прежний выбор не меняется. */ bool gui_stream_watch(uint64_t group, uint64_t owner = 0); -/* notify только будит worker; arg живёт до gui_stream_stop/detach. Callback не вызывает bridge/API ядра. */ +/* Ненулевые id/group/source/epoch; true — асинхронное открытие VIDEO|AUDIO принято, итог через Opened/Closed. + * id уникален до обработки stop actor. notify может вызываться сразу; arg живёт до gui_stream_stop/detach. + * Callback только будит worker, не вызывает bridge/API ядра. false — отказ открытия/установки notify. */ bool gui_stream_open(uint64_t id, uint64_t group, uint64_t source, uint64_t epoch, void (*notify)(void*) = nullptr, void* arg = nullptr); +/* Синхронно разбудить/снять notify и асинхронно закрыть viewer; старый id допустим, полученные records сохраняются. */ void gui_stream_stop(uint64_t id); +/* true — одна готовая запись записана в record. false — record не меняется; при наличии manager запрошена выдача. + * position<0 прогревает звук до KEY; иначе позиция playback в мкс. audioAhead=-1 отключает звук, иначе 0..60000000 мкс. + * Это режим выдачи по одной записи: не смешивать с pull_batch на том же id без сброса reader. */ bool gui_stream_pull(uint64_t id, StreamRecord& record, int64_t position = -1, bool wantVideo = true, int64_t audioAhead = STREAM_MEDIA_READ_AHEAD_US); -/* Добавляет готовый батч в records; при пустом ответе запрашивает до videoBudget кадров и audioAhead звука. - * Вызывать после расходования нужного трека: удерживаемые видеокадры не мешают запросить только звук. */ +/* true — готовый батч добавлен в records, прежние записи не удаляются. false — новых записей пока нет либо viewer недоступен. + * При пустом ответе запрашивает до STREAM_VIEWER_BATCH_RECORDS записей: videoBudget=0..16 кадров и audioAhead звука. + * position/audioAhead как у pull. Вызывать после расходования нужного трека; удерживаемое видео не мешает запросить только звук. + * Запрос исполняется асинхронно, false не отличает ожидание от отказа demand; ошибки scheduling логируются ядром. */ bool gui_stream_pull_batch(uint64_t id, std::deque& records, int64_t position, unsigned videoBudget, int64_t audioAhead); +/* true — последний снимок viewer, сохраняется после End/Closed до stop; до обновления actor/после resync — нули. + * false — ID/manager недоступен, выход не меняется. */ bool gui_stream_status(uint64_t id, stream_media_status& status); +/* true — снимок цепочки; раз в 200мс запрашивает обновление в uasync без ожидания его результата. + * До первого снимка нули; false не меняет telemetry. */ bool gui_stream_telemetry(uint64_t id, stream_viewer_telemetry& telemetry); +/* Очистить готовый ответ и запросить поиск ближайшего KEY только этого viewer; true — принято. */ bool gui_stream_resync(uint64_t id); +/* Есть прикреплённое ядро/managers; не означает открытие viewer/publisher или готовность сетевых маршрутов. */ bool gui_stream_ready(); -/* Publisher независим от viewer; lifetime ядра ограничивает общий bridge mutex. */ +/* Единственный publisher независим от viewers. Ненулевые id/group и допустимый format; true — принято OPENING. + * После Ended/Failed возможен новый id, результат открытия читается publish_status. */ bool gui_stream_publish_open(uint64_t id, uint64_t group, const stream_media_format& format); +/* true — mute/unmute принят в OPENING/LIVE без смены эпохи/аудиокодека; включение требует audio codec в format. */ bool gui_stream_publish_audio(uint64_t id, bool enabled); +/* 1 — data скопирован в очередь, 0 — нужен KEY/перегрузка, -1 — ошибка/старый id; приём не гарантирует доставку. + * pts<=INT64_MAX строго растёт отдельно по трекам; video требует полный access unit и format, audio — один Opus. + * Новый video revision принимается с KEY; format/key для audio игнорируются. data/format заимствованы только на вызов. */ int gui_stream_publish_feed(uint64_t id, bool video, uint64_t pts, bool key, const QByteArray& data, const stream_media_format* format = nullptr); +/* Запретить feed, дренировать очередь с TTL и удержать FIN 2с; в OPENING отменить открытие. Повтор/старый id допустимы. */ void gui_stream_publish_stop(uint64_t id); +/* true — последний снимок, включая ENDED/FAILED; false — id/manager недоступен, status не меняется. */ bool gui_stream_publish_status(uint64_t id, stream_publisher_status& status);