diff --git a/doc/stream.txt b/doc/stream.txt index ca137453..8d9a51ab 100644 --- a/doc/stream.txt +++ b/doc/stream.txt @@ -794,7 +794,7 @@ Desktop и Android открывают просмотр через stream_media_o Ретрансляторы не разбирают uAV2 и не пересоздают подписи источника. Android: -- Один foreground publisher на процесс, один viewer. Каждая операция имеет новый session ID. +- Один foreground publisher на процесс; каждый viewer имеет независимый session ID. - StreamService владеет камерой, подпиской на микрофон и PARTIAL_WAKE_LOCK; пересоздание Activity не завершает источник. Сервис camera|microphone запускается из видимого приложения после runtime-разрешений; без микрофона — camera. - Camera2 TEMPLATE_RECORD пишет в Surface encoder. Кодеки перечисляются в порядке H.264/H.265/VP9/AV1/H.266; @@ -816,8 +816,9 @@ Android: lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона, Opus или source_epoch; compressor не добавляет задержку и не меняет capture PTS. -- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder/AudioTrack — viewer worker. -- Native bridge использует общий stream_publisher для TX и собственный actor для viewer. +- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder — отдельный viewer worker, + общий AudioTrack — worker StreamAudioMixer. Перекладка плиток сохраняет TextureView/decoder. +- Native bridge использует общий stream_publisher для TX и stream_viewers для RX. stream_publisher держит pthread mutex, ограниченные очереди и один pending post либо media timer; статистика обслуживается отдельным таймером 100ms. Тот же модуль используется desktop. attach/detach выполняются в uasync вне callbacks. detach отменяет post/timer, закрывает handles, @@ -827,7 +828,8 @@ Android: неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms. При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY. Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms. -- RX<=4MiB/256 records, bridge готовит один ответ на незавершённый запрос worker. +- RX: stream_viewers держит отдельный handle/cursor, demand, status и один owned ответ на session ID. + Общей RX FIFO нет: остановка/отставание одного reader не задерживает остальных. Получение ответа и сетевые AVAILABLE не запускают автоматическое чтение следующего кадра. Запас остаётся в RS-кольце. want_video запрашивает следующий полный видеокадр независимо от часов показа; audio_ahead ограничивает только звук относительно PTS потребителя. Управление/звук обслуживаются независимо от video pressure. @@ -842,13 +844,15 @@ Android: - Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра. AudioTrack задаёт время показа видео по реально услышанному PCM; без аудио используются монотонные часы с остановкой на границе готовых кадров. - Android StreamAudioOutput принадлежит viewer worker и хранит один недописанный PCM (до 480 stereo frames). + Android StreamAudioOutput принадлежит StreamAudioMixer и хранит один недописанный общий PCM (до 480 stereo frames). Одна WRITE_NON_BLOCKING за проход: частичный/нулевой результат сохраняет остаток, не задерживая video pump. - Выдано native = принято устройством + остаток; часы используют playbackHeadPosition без экстраполяции timestamp. + Шкала каждого StreamMixedPlayback начинается на своём output frame; playbackHeadPosition переводится + в число реально услышанных кадров этого input. PTS разных источников не сравниваются. Заполнение поддерживается до фактического bufferSizeInFrames устройства; API>=31 задаёт start threshold=1. - При EOF недописанный PCM передаётся полностью; короткий хвост дополняется не более одного start threshold тишины. - Эта тишина не входит в native PTS; завершение ждёт воспроизведения всего принятого PCM, не окончания тишины. - Пауза сохраняет остаток; GAP освобождает output вместе с native timeline. Новый output начинает счётчики с нуля. + При EOF недописанный PCM передаётся полностью; mixer выдаёт тишину после последнего кадра источника + до его физического воспроизведения. END одного input не закрывает AudioTrack остальных. + Hold/starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS. + GAP создаёт новый input/origin, сохраняя общее устройство и часы остальных трансляций. Watchdog 2s проверяет отсутствие движения hardware head только при достаточном заполнении либо заблокированной записи; намеренная пауза и ожидание данных до порога запуска не считаются зависанием. Ошибка содержит состояние и счётчики устройства. Защитный guard минутного контроллера учитывает audio device lead и наблюдённое опережение decoder @@ -866,7 +870,8 @@ Android: GraphicsLayer преобразует только видео, не Surface/decoder/reader и не управление; жест продлевает таймер скрытия управления. Во время подключения/ошибки управление видно. Смена видимости не меняет геометрию TextureView и не пересоздаёт reader. Выход восстанавливает прежнюю requestedOrientation и видимость системных панелей; ON_STOP освобождает viewer. - Собственный источник не открывает viewer/cursor, decoder, AudioTrack и playback controller. + При одиночном выборе собственного источника используется прямой preview без reader/decoder. + В режиме всех трансляций собственный источник, если активен, также имеет обычную плитку. Preview ориентируется по сенсору камеры и текущему положению экрана; ручной поворот трансляции его не вращает. TextureView уже применяет ориентацию сенсора. UI Matrix компенсирует только -displayDegrees, размеры view учитывают relative rotation; повторно поворачивать на SENSOR_ORIENTATION нельзя. @@ -889,15 +894,18 @@ Android: Камера при закрытии preview продолжает работать; publisher от UI не зависит. Видимый player удерживает экран включённым. Preview ограничен оставшимся местом, кнопки управления не перекрывает. - Source list общий для Android/desktop: stream_sources_json в uasync использует stream_member_load - и stream_member_online. stream_watch_sources наблюдает один выбранный канал; group=0 снимает наблюдение. + и stream_member_online. stream_watch_sources(inst, group, owner) наблюдает канал от имени владельца; + group=0 снимает только его наблюдение. Owner=0 — выбранный чат; desktop-сетка имеет собственного owner. Начальный snapshot и изменения после Merkle/BGP передаются через CHAT_EVT_STREAM_SOURCES=58: group_id:8BE, valid:1, JSON с id(decimal string), epoch(hex string), self, name. События объединяются отложенным callback, одинаковый список повторно не отправляется. Таймера опроса нет. + valid=0 сохраняет предыдущий список UI и показывает недоступность; это не пустой успешный снимок. Обнаружение не создаёт подписку, media-буфер и не удерживает ядро в ACTIVE. -- В шапке канала значок виден при наличии источников; один источник открывается сразу, - несколько выбираются из списка по имени/ID. Пункт меню «Трансляции» также открывает управление публикацией. +- В шапке канала значок доступен и при пустом списке; меню содержит «Смотреть все активные трансляции» + и выбор отдельного источника по имени/ID. Пункт «Трансляции» также открывает управление публикацией. Выбранная эпоха проверяется actor до подписки и по фактически открытому handle. Перезапуск источника - требует нового выбора. Ошибка viewer kind=6 передаёт stream_close_reason в flags. + требует нового выбора в одиночном режиме; сетка заменяет только плитку с новой эпохой. + Ошибка viewer kind=6 передаёт stream_close_reason в flags. - Ручная ориентация доступна до запуска и во время своей трансляции: три компактные кнопки 0/90/270 градусов относительно обычного положения устройства. Поворот не зависит от текущего положения экрана; SENSOR_ORIENTATION и направление камеры учитываются при расчёте video rotation. Камера/режим/ориентация хранятся и подтверждаются одной конфигурацией. @@ -1016,9 +1024,9 @@ Desktop: Stop закрывает источники и audio callback, join обоих workers, затем FIN и ограниченный drain 2s. Удаление engine происходит до SoundManager и ядра; detach отменяет posts/timers и закрывает publisher в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются. -- Один StreamPlayerWindow на процесс. Он содержит имя источника, состояние подключения, громкость, - полноэкранный режим и закрытие. Выбор нового источника закрывает предыдущего reader. -- transport/stream_bridge владеет одним viewer, готовит один ответ на запрос и ограничивает RX 4MiB/16 записей. +- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник. + Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну. +- transport/stream_bridge владеет stream_viewers: независимый cursor и один ближайший ответ на reader. Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex. Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers. - stream_media_cursor собирает ближайший access unit выбранного кодека и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца. @@ -1027,7 +1035,7 @@ Desktop: Worker держит один рабочий AVPacket и не более двух готовых изображений. Следующий video request не ограничен часами показа. FFmpeg output обслуживается перед input; EAGAIN сохраняет рабочий packet и означает backpressure, не GAP. Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM. - Кадры сохраняются до достижения их PTS аудиочасами; при задержке GUI общий clock останавливается. + Кадры сохраняются до достижения их PTS аудиочасами; задержка показа удерживает clock только этого player. Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY. - Первый decoded frame разрешает движение clock/audio, защитный guard учитывает наблюдённый decoder lead. FIN отправляет decoder EOS; выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста. @@ -1040,6 +1048,30 @@ Desktop: Закрытие окна снимает только reader, собственная публикация сохраняется. Удаление канала сообщает STREAM_CLOSE_GROUP; остановка ядра снимает media и catalogue owners. - Без FFmpeg окно сообщает, что декодирование недоступно. Нужна desktop-сборка с HAVE_FFMPEG. + +Просмотр всех активных трансляций (desktop и Android): +- Добавление/удаление определяется каталогом текущего канала. Порядок существующих плиток стабилен; + добавление, удаление, смена эпохи и ошибки одного player не пересоздают остальные. + Пропавший источник дочитывает FIN/готовый хвост: дедлайн равен остатку media + 2s, не более 30s. + После удаления последней плитки окно остаётся открытым и ждёт новые источники. +- Desktop: квадратная сетка ceil(sqrt(count)), в том числе 2x2, 3x3, 4x4; при >16 расширяется дальше. + Android выбирает строки/колонки по максимальной площади вписанного видео, без принуждения к квадрату. + Плоский keyed Layout сохраняет Surface при перекладке. ON_STOP/закрытие освобождает readers; + Surface/SurfaceTexture освобождаются только после выхода decoder worker. + Перед Android-звонком новые viewers заблокированы; подготовка ждёт workers и закрытия общего AudioTrack вне UI-потока. +- PCM всех источников смешивается в один stereo 48kHz output: SoundManager на desktop, + StreamAudioMixer/AudioTrack на Android. Opus, SoundTouch, темп, PTS и A/V clock у каждого свои. + Общий limiter имеет быстрый attack и release 100ms; тихая новая плитка не уменьшает громкость остальных. + Дополнительного накопительного буфера перед микшером нет; размеры RS-колец не менялись. +- Ошибка отдельного decoder повторяется с backoff 0.5..10s; смена эпохи заменяет только этот reader. + Закрытие канала/сервиса и удаление участника не вызывают бесконечного retry. + Android пробует подходящие декодеры по очереди, включая software при исчерпании аппаратных; + непригодный codec/format остаётся с явной ошибкой в своей плитке. Реальная вместимость зависит от устройства. +- Проверки: test_stream_view — независимые readers/resync/stop, владельцы каталога, геометрия до 16 плиток, + сохранение player при перекладке, новая эпоха, пустое ожидание, два одновременно играющих FFmpeg/Opus player, + сумма/limiter общего выхода и независимое отключение input. test_android_stream — независимые native readers. + Kotlin StreamAudioMixTest/StreamGridLayoutTest и StreamAudioOutputTest проверяют отдельные origin, + частичные записи, сумму PCM и limiter, портретную/альбомную раскладку и вместимость до 40 источников. Проверки: test_stream_view — настоящее ядро, старые эпохи/cancel, задержанное чтение без роста RX, H.264 с поворотом и сменой revision без новой эпохи, Opus/громкость на настоящем null audio backend, непрерывные 30fps при физической audio latency 200ms, закрытие канала и полный баланс tracked allocations. Раз в секунду desktop viewer логирует decode/show FPS, diff --git a/doc/tasks.md b/doc/tasks.md index 575dc230..5bbc8488 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -4,6 +4,19 @@ ## Текущие задачи +[+] **Просмотр всех активных трансляций: desktop, затем Android** — 20.10.2026: + Пункт в меню канала открывает автоматически обновляемую сетку; пустое окно ждёт новые источники. + Desktop: 2x2/3x3/4x4 с расширением по числу плиток; Android: раскладка по доступной площади экрана. + Общий stream_viewers хранит независимые handles/cursors; перекладка не пересоздаёт действующие players. + Отдельные Opus/SoundTouch/PTS/аудиочасы каждого источника, сумма PCM в одном output с limiter. + GAP, завершение и ошибка одного источника не останавливают остальные; размеры RS-колец не менялись. + C/desktop и Android APK собраны; Qt stream_view 19/19, шесть stream/audio recovery целей и native Android PASS. + Android unit 129/129; check.sh 164/165: прежний encrypted_attachment phase=5/heartbeats=12001. + На физических телефонах многопоточный просмотр пока не проверен; сборки не устанавливались. + Контракты и ограничения: doc/stream.txt. Логи: /tmp/multiview-desktop-audit-test.log, + /tmp/multiview-desktop-final-tests.log, /tmp/multiview-android-native-final.log, + /tmp/multiview-android-final.log, /tmp/multiview-full-check.log. + [+] **Multicast с исходящими дескрипторами и входящими подписками** — 20.10.2026: Отдельный routing_layer/multicast.c; контракт и примеры использования — multicast.h. Номера 1..65535 выдаются последовательно без поколений; подписки на (группа, source, descriptor).