You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1148 lines
159 KiB

Модуль стриминга. Техническое задание, этап 1.
1. Цель и принятые решения
Модуль размещается в src/stream. Узлы получают поток данных от источника через других подписчиков группы,
передают его своим подписчикам и постоянно ищут более качественные пути доставки отдельных субпотоков.
Этап 1 использует CHAT-группы с таблицей мемберов и member_sync/merkle_sync.
Принятые решения:
- Транспорт — существующий ETCP с быстрыми переповторами, мультилинком, шифрованием и управлением нагрузкой.
- Параметры FEC задаёт источник; 6+2 — значение по умолчанию, а не ограничение протокола.
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
- Передаём произвольные данные как есть, без обработки форматов видео, кадров и ключевых точек.
- Размер части автоматически уменьшается по длине пачки, максимум по умолчанию 1300 байт.
Побайтовый источник сбрасывает остаток через 100 мс; publish_block публикует сразу.
- На каждый локальный экземпляр трансляции — кольцевой буфер 32 МБ для всех её субпотоков вместе.
Вся выдача идёт из этого буфера. Вытесненные данные недоступны: не успел получить — опоздал.
- Отдельных дозапросов диапазонов нет. Единственный способ получения — запросить субпоток с индекса или с конца.
- Обратное подключение — NCD + REVERSE_REQ стриминга через маршрутизатор группы.
- Статус источника и список подписок — опции самого мембера, распространяемые через Merkle.
Удалённая подписка актуальна только пока узел онлайн в BGP данной группы.
- Экземпляры определяются локальными подписками; узлы без подписки не принимают и не ретранслируют поток.
Активный источник также имеет собственный экземпляр и подписку на свою трансляцию.
- Подтверждение перегрузки — 500 мс. Число отключаемых выдач определяется ростом очереди и rate отдельных выдач.
- При запуске параллельно пробуем до 32 совместимых кандидатов; далее одна проба в секунду,
новый полный цикл не начинается раньше чем через 30 секунд от начала предыдущего.
Обратная совместимость с прежними форматами не требуется.
2. Трансляция, подписки и владение ресурсами
Трансляция определяется ключом (group_id, source_node_id, source_epoch).
source_epoch — новый случайный ненулевой 64-битный идентификатор при каждом запуске источника.
В этапе 1 у одного источника в одной группе активна одна трансляция. Старые и новые запуски различаются по эпохе.
Пакеты и уведомления старого запуска не могут менять состояние нового.
Локальная подписка на трансляцию создаёт один экземпляр, общий для получения и ретрансляции.
Подписка на трансляцию отличается от сетевой выдачи конкретного субпотока по конкретному handle.
Один экземпляр может получать субпотоки от нескольких пиров и выдавать их нескольким получателям.
Повторная локальная подписка не создаёт второй буфер; разные локальные потребители используют один экземпляр.
Закрытие последнего локального владельца подписки останавливает экземпляр и все его сетевые выдачи.
Экземпляр хранит:
- Дескриптор источника, FEC-контекст, состояние и номер эпохи.
- Собственные NCD handles, независимо от владельцев транспорта в других модулях и трансляциях.
- Upstream-выдачи: поставщик, handle, субпоток, текущая позиция, путь, прогресс и метрики.
- Downstream-выдачи: получатель, handle, субпоток, курсор в кольце и срок аренды.
- Кольцевой буфер, индекс наличия блоков, состояния сборки пачек и статистику.
- Лимит числа одновременно выдаваемых субпотоков.
Одна выдача — один субпоток одному получателю. Один субпоток двум получателям занимает два исходящих слота.
Есть также общий лимит исходящих выдач модуля: лимиты разных экземпляров не должны суммироваться без ограничения.
Модуль хранит общий контекст пира в пределах UTUN_INSTANCE: одну очередь отправки стриминга для всех его
трансляций и групп, waiter на транспорт, stream-сессию, метрики и текущий запрос разгрузки.
Увиденная удалённая опция мембера не создаёт локальную подписку, буфер или NCD-подключение.
Без локального экземпляра dispatcher отбрасывает DATA после минимального разбора ключа трансляции:
не проверяет подпись содержимого, не выполняет FEC, не буферизует и не пересылает блок.
INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получает явный отказ, а не создаёт неявную подписку.
Операции с экземплярами, NCD, очередями, Merkle и таймерами выполняются в uasync-потоке ядра.
Внешние producers передают байты через существующий механизм post с ограничением ожидающего объёма.
При нехватке ресурсов создание подписки завершается явной ошибкой с очисткой частично созданного состояния.
3. FEC, номера блоков и размер кодограммы
Источник формирует пачки и кодирует их lib/reed_solomon:
- n — полное число субпотоков, r — число проверочных, k = n-r — число исходных.
- По умолчанию n=8, r=2, k=6. Любые k кодограмм одной пачки восстанавливают её содержимое.
- subblock от 0 до n-1 определяет субпоток; первые k блоков систематические, остальные проверочные.
- batch_seq — общий 64-битный индекс пачки; блок субпотока X определяется парой (batch_seq, X).
Позиция N в запросе — batch_seq, а не байтовое смещение или отдельный счётчик субпотока.
- Номера возрастают без повторов; UINT64_MAX зарезервирован для запроса с конца.
Переполнение требует новой source_epoch.
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
Ограничения кодека: 1 <= n <= 255, 0 <= r < n, 10 <= block_size <= 4096.
Кроме допустимости RS проверяются расход памяти и место для подписанных метаданных, подписи и хотя бы одного байта содержимого.
Максимальный размер части субпотока по умолчанию — 1300 байт: внешние метаданные 47 и RS до 1253 байт.
Внутри RS находятся подписанные метаданные 55 байт, содержимое и одна 64-байтная подпись.
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 6+2 — 7345 байт.
При длине содержимого L фактический RS-size = 9 + ceil((55 + L + 64)/k), полный размер части = 47 + RS-size.
Например, L=1000 даёт восемь частей по 243 байта; L=7345 даёт восемь частей по 1300 байт.
Один контекст RS принимает пачки разной длины без пересоздания матрицы; все части одной пачки равны по размеру.
Handle получателя, поколение stream-сессии, изменяемый путь и служебные заголовки ETCP идут сверх этого бюджета.
Не обещаем соответствия одной кодограммы одному UDP-пакету: необходимую фрагментацию выполняет ETCP normalizer.
На первой порции данных неполной пачки источник запускает таймер 100 мс.
При заполнении пачка немедленно кодируется, таймер отменяется; для следующей неполной пачки начинается новый срок.
По таймауту кодируем накопленные данные в минимальные части с дополнением максимум k-1 нулями.
Действительная длина содержимого входит в подпись; подпись также защищена RS от пропусков.
Без накопленных данных пустые пачки не создаём. При штатном окончании источник сразу сбрасывает остаток.
Этот таймер есть только у источника; ретрансляторы не накапливают данные для повторного формирования пачек.
В этапе 1 ретранслятор пересылает полученные блоки; отсутствующие блоки для downstream заново не генерирует.
Локальный decode восстанавливает содержимое и подпись для потребителя; отсутствующие части в кольцо не добавляются.
Восстанавливаем при наличии k структурно допустимых частей, не ожидая остальных.
После успешной проверки одной подписи выдаём только содержимое, без метаданных/подписи/padding, по порядку batch_seq.
Подпись не зависит от получения определённого субблока. Неверная подпись/подписанные поля дают GAP одной пачки;
приём следующей пачки продолжается, owner не закрывается. Повторно отвергнутая пачка не декодируется.
Подмена отдельной части обнаруживается только после RS; определить виновную часть по общей подписи нельзя.
Перебора альтернативных наборов и исправления неизвестных ошибок нет: RS исправляет только пропуски.
Невосстановимый пропуск обозначается событием GAP; нельзя молча склеивать данные по обе стороны разрыва или выдавать нули за исходные байты.
Окончательно пропускаем пачку, когда её данных уже нет в окне либо все нужные выдачи прошли её индекс/сообщили пропуск,
а k блоков не собрано. Получение более поздней пачки от одного поставщика само по себе не закрывает ожидание остальных.
Модуль не интерпретирует содержимое и не занимается видео, кодированием, воспроизведением или поиском ключевых кадров.
4. Подпись источника и timestamp
Источник подписывает Ed25519 каноническое сообщение stream_batch: group_id, source_node_id, source_epoch,
n, r, фактический RS-size, source_ts_ms, действительную длину, flags, batch_seq и все байты содержимого.
Единственная подпись добавляется к этому сообщению до RS. Внешняя часть stream_part1 содержит копию полей
без криптографической проверки; seq/subblock идут в RS-заголовке. Формат big-endian закреплён в stream_proto.h.
После decode подпись проверяется по ключу источника, а подписанные поля сверяются с дескриптором и внешними подсказками.
Неизменяемые части хранятся и пересылаются без изменения и без отдельной подписи каждого субблока.
Handle, stream-сессия и hop-list находятся вне подписанного тела, поскольку меняются при выдаче и ретрансляции.
Перед помещением части в кольцо проверяются формат, границы размеров, канонический размер и согласованность полей пачки.
Части могут пересылаться до сборки всей пачки, сохраняя малую задержку; они ещё не подтверждены ключом источника.
Содержимое для приложения допускается только после проверки подписи по ключу из принятой записи мембера данной группы.
Ключ непосредственного поставщика не заменяет ключ источника. Неверная подпись, неизвестный ключ или конфликт логируются.
При отвергнутой пачке логируются выбранные субблоки и их поставщики; приложение получает GAP и продолжает работу.
Дубликатом считается только побайтовое совпадение всей части. Тот же индекс с другим содержимым — конфликт.
source_ts_ms = (uint16_t)monotonic_time_ms источника в момент формирования пачки.
У всех её субпотоков одинаковые timestamp, длина и флаги. Ретранслятор не переписывает их своим временем.
Timestamp циклический с периодом 65536 мс; обычный переход через ноль обрабатывается модульной разностью.
Это не Unix-время; NTP и сравнение абсолютных часов разных узлов для метрики задержки не требуются.
16 бит не позволяют однозначно восстановить произвольное число полных оборотов.
Источник помечает в подписанных флагах разрыв временной шкалы, если интервал между его пачками >= 65536 мс.
При таком флаге, смене поставщика/эпохи, потере непрерывности индексов или длинном перерыве приёма
получатель начинает новую базу измерения, не придумывая число оборотов.
Первый блок устанавливает базу и не измеряет абсолютную задержку пути.
5. Кольцевой буфер и единственный способ выдачи
На экземпляр — 32 * 1024 * 1024 байт под неизменяемые части всех субпотоков вместе.
Слоты резервируются по максимальному размеру; меньшие части уменьшают трафик/работу RS, но не размер массива слотов.
stored_bytes и логическая очередь выдач учитывают фактические длины. Вытеснение ограничено числом зарезервированных слотов.
Метаданные индекса, путей и сборки учитываются отдельно и ограничиваются.
Буфер общий для локального потребителя и всех downstream; копии кольца на каждого получателя не создаются.
Источник сначала помещает сформированные блоки в своё кольцо, затем делает их доступными выдачам.
Ретранслятор сначала проверяет формат и сохраняет часть, затем планирует выдачу подписчикам.
Индекс записи — (batch_seq, subblock) внутри конкретной эпохи.
Новые пачки вытесняют старейшие; при вытеснении пачки удаляются все её блоки и метаданные.
Дубликаты не занимают повторную память и не продлевают хранение.
Блоки старше нижней границы окна не вытесняют более свежие данные.
По каждому субпотоку известны доступные границы и наличие блоков внутри них; между границами возможны дырки.
Длительность истории определяется битрейтом и FEC; фиксированного времени хранения нет.
Есть только SUBSCRIBE(subblock, N):
- Конкретный N: начать непрерывную выдачу этого субпотока с указанного индекса, затем выдавать новые блоки.
- N=UINT64_MAX: начать с последнего доступного блока запрошенного субпотока, затем продолжать выдачу.
Нет REPAIR_REQ, выдачи диапазонов и отдельной системы повторного запроса потерянных блоков.
Выдача хранит курсор и читает только кольцо; запрос не закрепляет данные в памяти и не резервирует историю.
Если начальный блок уже вытеснен или недоступен, возвращаем явный отказ с границами окна.
Недоступную позицию не заменяем последней молча.
Если курсор работающей выдачи отстал за вытесненные данные, сообщаем EXPIRED/GAP для пропуска и продолжаем с доступного индекса.
Если внутри окна нет блока, но уже доступны более поздние, сообщаем GAP и продолжаем.
Восстановление получения выполняется выбором поставщика/новым SUBSCRIBE с нужного индекса, пока он ещё есть в его кольце.
Никакого отдельного архивирования, гарантии хранения до доставки или ожидания медленного получателя нет.
Общая очередь хранит ключ блока и идентификатор выдачи; долговременно указатели внутрь переиспользуемого кольца не сохраняются.
Перед передачей в ETCP наличие проверяется заново, пакет получает самостоятельное содержимое.
Вытесненное задание учитывается как пропуск, отменённое не отправляется.
После CLOSED не отправляются новые DATA/GAP по старому handle.
Уже принятые транспортом пакеты принадлежат ETCP и не ссылаются на память кольца.
6. Транспорт и NCD + reverse
Данные и управление установленной выдачей идут непосредственно между поставщиком и получателем через raw ETCP.
Для стриминга выделяются новые IDs в едином реестре src/transport_layer/etcp_api.h:
raw ID прямого обмена и router service ID управления через группу, без конфликтов в соответствующих пространствах.
ETCP обеспечивает переповторы, порядок, мультилинк и backpressure; второй механизм транспортных ACK поверх него не создаётся.
До отправки требуется UP от собственного NCD handle. Наличие ETCP_CONN само по себе недостаточно.
Согласование BGP-сессий остаётся в topo_group; stream приобретает своё NCD-владение и не дублирует этот протокол.
Членство проверяется отдельно; NCD такую проверку не выполняет.
На стороне отправителя до ACCEPT приобретается собственное NCD-владение на срок выдачи.
Прямое подключение запускается через node_conn_direct_open().
При необходимости инициатор отправляет REVERSE_REQ через маршрутизатор группы;
получатель проверяет членство, наличие своей подписки на эту трансляцию и открывает встречный NCD handle.
Запрос содержит ID попытки и ключ трансляции; адреса и ключ пира берутся из проверенного реестра.
Повтор одной попытки не создаёт дополнительные handles; сроки и повторы ограничены.
Завершение/отмена попытки убирает её таймеры и временное владение.
Совместимость учитывает оба узла, локальные сокеты, типы NAT, LAN/публичные IPv4/IPv6 и direct/reverse.
Используются существующие критерии sock_match/NCD; отбор не строится только по наличию EIM у одного пира.
Код адресного отбора, handshake, шифрования и линков в stream не дублируется.
Маршрутизатор группы обслуживает REVERSE_REQ и уведомления при отсутствии прямой связи,
а не большую INDIRECT-выдачу через conn_mgr.
7. Протокол и сроки выдач
Основные команды:
- INFO_REQ/INFO_RESP — дескриптор, доступные субпотоки, диапазоны кольца, пути, rates и метрики кандидата.
- SUBSCRIBE/ACCEPT/REJECT — непрерывная выдача с индекса либо последнего доступного блока.
- RENEW — продление списка существующих выдач, период 2 секунды.
- UNSUBSCRIBE/CLOSED — закрытие выдачи с указанием handle и причины.
- DATA/GAP/EXPIRED — часть подписанной RS-пачки либо уведомление о пропуске.
- OVERLOAD — запрос сокращения исходящей нагрузки через закрытие нескольких выдач.
- REVERSE_REQ — встречное NCD-подключение через маршрутизатор группы.
- END — последняя пачка и штатное завершение содержимого источником.
handle — ненулевой 64-битный ID, назначаемый получателем.
Механизм защиты от повторов: возрастающие handles в пределах stream-сессии пира,
новые SUBSCRIBE отправляются по порядку в одной управляющей FIFO.
Номер выделяется непосредственно при постановке нового SUBSCRIBE в эту FIFO, после готовности подключения;
параллельные ожидания NCD/INFO не резервируют номера, которые позже нарушили бы порядок отправки.
Ключ выдачи — peer, stream key, поколение stream-сессии и handle.
Повтор известного действующего SUBSCRIBE с прежними параметрами возвращает прежний результат,
не создаёт вторую выдачу, не меняет курсор и продлевает аренду.
Другие параметры по тому же handle отвергаются.
Для закрытого/неизвестного handle не выше уже обработанного номера SUBSCRIBE — STALE_REQUEST.
Водяной знак сохраняется до конца сессии; не нужен неограниченный список всех ранее закрытых handles.
RENEW создаёт только продление существующей выдачи. Повторное закрытие безопасно.
Для другой позиции создаётся новый handle; после переполнения счётчика требуется новое поколение сессии.
Поколения пиров согласуются при прямом INFO-обмене до SUBSCRIBE.
При DOWN, REINIT или закрытии старое поколение инвалидируется вместе с его выдачами.
Поздние ACCEPT/DATA/routed CLOSED не восстанавливают их. ETCP_CONN может пережить REINIT, его адрес не является ID сессии.
При REINIT или смене удалённой эпохи исходящий запрос CONNECTING/INFO без SUBSCRIBE-handle сохраняется:
его INFO-состояние и nonce очищаются, согласование продолжается с новым nonce и исходным сроком попытки.
При DOWN, окончательном закрытии транспорта или ошибке отправки такой запрос отменяется.
Сохранённый запрос с истёкшим сроком закрывается; повторные REINIT не продлевают его ожидание.
Управляющие поля и размеры списков имеют явный byte order и пределы.
ACCEPT содержит handle, фактический начальный индекс, границы кольца и путь субпотока.
Реальное получение блока, а не только ACCEPT, подтверждает пригодность выдачи.
Индекс сразу после последнего доступного блока разрешён: курсор ждёт его появления в том же кольце.
Более далёкий будущий индекс отвергается. Пустой источник допускает выдачу с 0/с конца,
которая ждёт первый блок; первый проверенный DATA по-прежнему устанавливает общий N.
Пустой ретранслятор пути не объявляет. Завершение без данных подтверждается подписанным END с last_seq=UINT64_MAX.
Отказы различают отсутствие подписки/трансляции, эпоху, FEC, позицию, слот, перегрузку, цикл и право доступа.
DATA содержит поколение сессии, handle, неизменяемую часть RS-пачки и изменяемый путь.
Источник подписывает и END с ключом трансляции и индексом последней пачки, чтобы ретранслятор не мог подделать окончание.
Нет продления 6 секунд — отправитель закрывает выдачу и сообщает CLOSED, при необходимости через маршрутизатор группы.
Аренда, пробы и timers используют монотонное локальное время. Таймаут аренды очищает ресурсы;
реакция на отсутствие прогресса начинается раньше по метрикам потока.
Потеря отдельного ETCP-линка не закрывает выдачи, пока NCD остаётся UP.
DOWN закрывает выдачи соответствующей сессии; REINIT требует их согласовать заново.
Локальное освобождение ресурсов не ждёт доставки уведомления.
8. Начальная позиция и перенос субпотоков
Начальный запуск:
1) По опциям мемберов и BGP выбрать первый подходящий онлайн-узел, включая сам источник.
2) Запросить один субпоток с N=UINT64_MAX.
3) Получить и проверить первый блок, зафиксировать его source_epoch и batch_seq=N.
4) У остальных поставщиков запросить субпотоки этой же эпохи с конкретного N.
5) Собрать достаточный набор блоков одной пачки, затем продолжать непрерывное получение.
Не выбирать независимый последний индекс у каждого поставщика: пачки должны совпадать.
Если N успел исчезнуть из колец или набор собрать нельзя, явно начать новую попытку с конца одного поставщика.
Не смешивать старую и новую попытки и не выдавать их за непрерывное содержимое.
Обычный перенос одного субпотока:
1) Выбрать первую ещё нужную пачку N и поставщика, способного её выдать.
2) Создать новую выдачу, продолжая старую; временное перекрытие также занимает исходящий слот нового поставщика.
3) Получить блоки и подтвердить продвижение нового поставщика.
4) Закрыть старую выдачу. При отказе новой сохранить старую и отменить неудачную попытку.
Дедупликация — по (stream key, batch_seq, subblock), независимо от handles.
При перекрытии качество поставщиков учитывается отдельно, но блок потребителю выдаётся один раз.
При явном отказе разрешено одновременно восстанавливать несколько субпотоков.
Точная задержка обычного переноса и гистерезис задаются настройками; небольшое колебание оценки не запускает перестройку.
9. Пути и защита от циклов
У каждого субпотока свой stream-путь от источника до поставщика; BGP-путь его не заменяет.
INFO_RESP и ACCEPT содержат путь запрошенного субпотока.
Путь начинается источником, не содержит нулей и повторов; подписка запрещена, если получатель уже находится в нём.
Эта проверка повторяется при принятии запроса, а не только при предварительном выборе.
DATA несёт фактически пройденные узлы; начальный путь содержит источник, последний — непосредственного проверенного отправителя.
При пересылке добавляется свой node_id. Повторное посещение и превышение максимума хопов запрещены.
При выдаче из кольца сохраняется история пути блока; запрещено отправлять его уже пройденному узлу.
В связи с этим отсутствие пригодного блока для конкретного получателя также обозначается пропуском.
Смена upstream обновляет объявляемый путь и повторно проверяет downstream-выдачи; возникший цикл закрывается с причиной LOOP.
При гонках метрик временно возможна неработающая цепочка, но DATA не циркулирует, а отсутствие прогресса запускает поиск.
Старые данные в кольце сами по себе не делают узел качественным live-поставщиком.
10. Относительная задержка, остановка и rate
Метрики ведутся отдельно для каждого поставщика и субпотока.
Обрабатываются новые индексы, а не дубликаты; timestamp всех субпотоков одной пачки одинаковый.
Для последовательных блоков:
source_delta_ms = (uint16_t)(source_ts_current - source_ts_previous)
receive_delta_ms = local_receive_current_ms - local_receive_previous_ms
lag_ms = max(0, lag_ms + receive_delta_ms - source_delta_ms)
Первый блок устанавливает базу. Обычный оборот timestamp обрабатывается без скачка lag.
Так измеряется накопленное отставание относительно скорости источника, а не абсолютное время от источника до получателя.
Ускоренный приход уменьшает lag, но значение не становится отрицательным.
Источник timestamp не изменяется по пути, поэтому изменение доставки учитывает задержки на всей цепочке.
Базу сбрасываем при отмеченном источником разрыве времени, неоднозначной дельте, новой выдаче/эпохе или разрыве последовательности.
Резкое изменение позиции выдачи не считается доказательством улучшения задержки.
Ожидаемый межблочный интервал и rate источника оцениваются по окну его timestamp и объёмам блоков.
Если несколько блоков имеют один timestamp, агрегируем объём до ненулевой дельты, не делим на ноль.
Не используем время формирования всех n блоков одной пачки как n независимых временных интервалов.
При отсутствии новых данных продолжаем измерять отставание от ожидаемого следующего прихода:
stall_lag_ms = lag_ms + max(0, now_ms - last_receive_ms - expected_interval_ms)
Превышение 500 мс является проблемой доставки; полная остановка не остаётся незамеченной только потому, что новых timestamp нет.
Отсутствие данных источника и остановка на маршруте по одной этой метрике неразличимы:
фиксируем отсутствие прогресса, не приписывая ему недоказанную причину. END прекращает ожидание новых блоков.
История проблем — 60 секундных корзин: число запаздывающих блоков и отдельные эпизоды остановки.
Блок считается запоздавшим один раз для соответствующей выдачи; один эпизод остановки не считается заново на каждом тике.
Сохраняются число наблюдаемых блоков, rates и доля проблем, чтобы сравнение не искажалось разным битрейтом.
FEC-восстановление не считается доставкой отсутствующего субпотока; частичные пачки и пропуски считаются отдельно.
Скорость продвижения по старой истории при запуске/переносе отличается от steady live и явно отмечается.
INFO_RESP также сообщает число свободных исходящих слотов, диапазоны буфера, актуальность прогресса, очередь и нагрузку.
Суммарный RTT = RTT непосредственного соединения + объявленный RTT stream-пути кандидата, отдельно по субпотоку.
Поле суммарного RTT — не менее 32 бит с явными единицами и признаком неизвестного/устаревшего значения.
11. Поиск поставщиков и расписание проб
Кандидаты — онлайн в BGP данной группы, с подпиской на нужную трансляцию/эпоху в опциях мембера.
Начальный поиск учитывает источник. Фоновые пробы ретрансляторов исключают мобильных.
Мобильный узел может быть источником и получателем; выбранный мобильный источник допускается к подключению.
Активное получение/публикация согласовывается с активностью ядра, чтобы standby не прерывал поток.
Локальные owners удерживают эффективный ACTIVE до последнего close, включая сохранённый буфер после END.
Запрошенный frontend режим хранится отдельно: STANDBY не отменяет стрим, а применяется после снятия последнего владения.
При запуске экземпляра одновременно пробуем до 32 совместимых по типу подключения кандидатов.
Если подходящих меньше, пробуем имеющихся. Это узлы, а не 32 отдельных адреса или субпотока.
Совместимость определяется существующими транспортными правилами; известный рабочий транспорт переиспользуется.
После стартовой серии запускаем не более одной новой пробы в секунду.
Один кандидат пробуется один раз за цикл; новый полный цикл начинается не раньше 30 секунд от старта предыдущего.
Если кандидатов мало и цикл исчерпан раньше, до этого срока ждём. Если их много, цикл занимает фактическое время обхода.
Стартовые пробы учитываются в первом цикле, а не повторяются сразу после него.
Новые кандидаты из Merkle/BGP могут добавляться в текущий цикл с темпом 1/с, без повторного опроса уже проверенных.
Неуспех имеет backoff; одинаковые активные пробы одного пира для разных экземпляров переиспользуются.
Временные NCD handles невыбранных кандидатов освобождаются; рабочие экземпляры удерживают свои handles.
Используются route_connectivity, sock_match/NCD и метрики существующих соединений; второй адресный probe-движок не создаётся.
Прямая доступность — UNKNOWN/REACHABLE/UNREACHABLE с временем замера, а не вечный boolean.
После проверки доступного субпотока/индекса и отсутствия цикла минимизируем интегральную оценку в миллисекундах:
score_ms = RTT_цепочки_до_поставщика + 2 * RTT_до_поставщика + 3 * hop_count + problem_penalty_ms
hop_count включает последний переход от поставщика к получателю; для прямого получения у автора равен 1.
problem_penalty_ms = min(1000, доля_поздних_блоков * 1000 + (были_остановки ? 10 : 0)), с округлением вниз до мс.
Текущая остановка или lag > 500 мс дают штраф 1000 мс; неизвестные метрики дают 0, неизвестный RTT — бесконечную оценку.
При равенстве оценок выбираем меньше общих узлов с остальными путями, затем меньший ID поставщика.
Стремимся получить n субпотоков, хотя k достаточно для decode.
При возможности не более r субпотоков от одного поставщика: потеря его при получении всех n оставляет не менее k.
При r=0 такой защиты нет; при недостатке кандидатов ограничение ослабляется с отражением устойчивости в статусе.
Учитываем совпадение путей разных поставщиков, кроме общего источника.
12. Общая отправка и перегрузка
На удалённого пира — одна ограниченная очередь стриминга для всех групп и источников.
Управление имеет небольшой приоритет; выдачи данных чередуются, чтобы один источник не захватил всю очередь.
При старте с прошлого индекса не помещаем сразу всё кольцо в очередь: читаем его по курсору по мере готовности отправки.
Темп задают имеющиеся данные и backpressure ETCP, независимый транспортный shaper не создаётся.
Отправка ждёт освобождения conn->send_input_q через queue_waiter_wait с существующим порогом.
Не переполняем очередь, не меняем чужой общий порог и не заменяем waiter активным опросом.
Handle waiter обнуляется полностью, callback может быть синхронным; отменяем waiter до освобождения владельца.
При автозаборе ll_queue соблюдается queue_data_get -> обработка -> queue_resume_callback.
При успехе etcp_send пакет принадлежит транспорту; при ошибке его освобождает stream.
Принятие стеком не означает доставку; для диагностики учитываются нижние очереди/inflight и продвижение приёмника.
Уже принятые ETCP пакеты не извлекаются обратно; общий транспорт ради одного потока не сбрасывается.
Раз в 200 мс сохраняется интервал, история — 10 секунд.
Длительность интервала фактическая, включая задержки uasync.
По каждому пиру и выдаче считаем пакеты/байты:
A — поступившая нагрузка, D — передано в ETCP, X — отменено/вытеснено, Q — ожидающий объём;
баланс Q_next = Q + A - D - X, возраст старейшего задания, rate и скорость роста.
Для заданий-ключей байтовый объём — размер ожидающего блока, память самих заданий учитывается отдельно.
Также сохраняются отказы при постановке, чтобы ограничение очереди не скрывало реальный избыток нагрузки.
Подтверждение перегрузки — непрерывные 500 мс наблюдаемого отставания, по монотонному времени, не по округлённому числу тиков.
Возраст данных >200 мс вместе с устойчивым ростом — сигнал недостаточной скорости.
Полная остановка D также обрабатывается. Краткий глитч с последующей быстрой разгрузкой не вызывает немедленного отключения.
Нагрузка uasync берётся из uasync_get_load(); дополнительно измеряется опоздание callbacks.
Проблема одного пира отличается от одновременного роста многих очередей и локального отставания event loop.
Высокий CPU отдельно от отставания/очередей не является достаточным основанием.
Скорость роста g оценивается в байтах/с по увеличению Q до искусственных удалений:
в интервале без смены выдач g = max(0, (A-D)/elapsed_seconds).
Rate выдачи — её средний вклад в поступающую нагрузку, а не урезанная скорость выхода из перегруженного канала.
Для разгрузки нужно снять суммарный rate больше g, чтобы очередь начала убывать.
Число выдач выбирается по их rates: закрываем достаточно выдач для требуемого сокращения;
при сильном росте это сразу несколько, не фиксированная одна и не случайное постоянное число.
OVERLOAD содержит request_id, расчётное число выдач, требуемое сокращение rate и список handles с фиксированными rates.
Получатель выбирает handles с учётом FEC и альтернативных поставщиков и отправляет OVERLOAD_SELECT.
Отправитель исполняет выбор, только пока актуален исходный request_id, и сообщает CLOSED.
Получатель не отсоединяет выбранные handles до CLOSED: поздний SELECT после принудительного закрытия не режет дополнительные выдачи.
При разных rates одного количества закрытий недостаточно: суммарный снятый rate должен покрыть требование.
Rate закрываемых выдач фиксируется по оценке на момент запроса, чтобы условие исполнения не менялось на каждом тике.
Срок реакции — 2 секунды с первого запроса; повторы не сбрасывают срок и не начинают новое требование.
Новые выдачи, компенсирующие запрошенное сокращение, до разгрузки не принимаются.
Если требование не выполнено, отправитель сам закрывает достаточный остаток выдач и сообщает handles/причину.
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку, а при равенстве допустима случайность.
При глобальном отставании сокращение выбирается по всем пирам модуля.
Память и возраст очереди остаются ограниченными во время ожидания; просроченные задания удаляются с GAP/EXPIRED.
13. Статусы через опции мембера и Merkle
Статус источника и список подписок принадлежат самому мемберу в конкретной группе.
Они входят в его подписанный блок A/userinfo и распространяются существующим member_sync/merkle_sync.
adm_tags — блок владельца, для самоуправляемых stream-опций он не используется.
Отдельного STREAM_ANNOUNCE, broadcast-обновлений и собственных TTL/версий анонсов нет.
Опции содержат:
- stream_source: активна ли собственная трансляция; если активна — её source_epoch и параметры FEC.
- stream_subscriptions: список (source_node_id, source_epoch), на которые узел локально подписан.
Формат — совместимые с существующим плоским userinfo строковые значения; список кодируется однозначно,
со строгим разбором 64-битных IDs без потери точности через JSON numbers.
Зафиксированный формат:
stream_source = "<source_epoch:16 lowercase hex>:<n:decimal>:<r:decimal>:<block_size:decimal>"
stream_subscriptions = "<source_node_id:16 lowercase hex>:<source_epoch:16 lowercase hex>[:r],..."
Суффикс :r означает внутренний приём только ради помощи, без локального зрителя.
Локальный viewer на общем экземпляре снимает :r; закрытие последнего viewer возвращает :r, если помощь ещё используется.
Отсутствующая/пустая опция означает отсутствие активности. Подписки отсортированы по source_node_id, затем эпохе;
нулевые IDs/эпохи, дубликаты, неверные разделители, ведущие нули decimal и переполнение отвергаются.
Активная публикация требует записи собственной трансляции в stream_subscriptions.
Максимум элементов списка — MS_USERINFO_MAX/34; фактически доступное число дополнительно ограничено
размером всего JSON с именем, аватаркой и прочими сохранёнными опциями. Две stream-опции меняются атомарно.
Источник включает свою трансляцию в собственные подписки.
Список относится к трансляциям, а не к сетевым handles или выбранным upstream-поставщикам.
Изменение подписки/запуск/остановка источника обновляет свой блок мембера:
userinfo + возрастающий update_ts + update_sig, атомарно с Merkle-хешами, после commit запускается распространение.
Другие опции (имя, аватар и т.д.) сохраняются; запись одного модуля не должна стирать состояние другого.
Размер укладывается в MS_USERINFO_MAX; переполнение — явная ошибка подписки, а не молчаливое обрезание.
При запуске ядра свои stream-опции приводятся к реально восстановленному локальному состоянию,
чтобы старый источник/подписка после аварии не объявлялись работающими без экземпляра.
Подписка удалённого мембера актуальна только если есть его живой BGP-путь в этой же группе.
Недостаточно присутствия записи в SQLite/реестре или произвольного ETCP-соединения из другой группы.
При исчезновении BGP-пути узел исключается из кандидатов; его подписанная опция в Merkle не удаляется третьей стороной.
При возвращении в BGP опции читаются заново и текущая доступность подтверждается INFO.
Статус источника также проверяется совместно с его доступностью; наличие старого source_epoch в БД не доказывает поступление данных.
Stream следит за двумя независимыми событиями: изменением блока мембера и изменением BGP-доступности.
Текущий props callback передаёт adm_tags; stream-опции читаются из принятого userinfo после commit,
а не трактуются как поля adm_tags.
Merkle уже обеспечивает версии, подписи и обмен для поздних участников; второй механизм хранения объявлений не создаётся.
Rates, RTT и очереди запрашиваются прямо у кандидата, а не записываются в Merkle каждые 200 мс.
Входящий SUBSCRIBE проходит авторизацию по членству; задержка распространения его опции не должна создавать взаимное ожидание.
Сам получатель обязан иметь локальную подписку до запросов, а выдающий узел — свою до обработки потока.
14. Жизненный цикл и диагностика
При остановке последнего локального владельца прекращаем producers/поиск, снимаем опцию подписки,
отменяем callbacks/таймеры/waiters, закрываем сетевые выдачи, убираем задания и освобождаем свои NCD handles.
Кольцо и FEC-контекст освобождаются после прекращения обращений.
Работа других трансляций/модулей на общем ETCP сохраняется.
Удаление группы, исключение мембера и смена source_epoch отменяют относящиеся к ним выдачи.
Stop/destroy выполняются по правилам ядра, вне callbacks останавливаемого сервиса.
Категория stream с конфигурируемым уровнем:
- INFO: запуск/остановка с FEC/размером/лимитами, выдачи, перенос, подтверждённая перегрузка и её результат, итоговые счётчики.
- DEBUG: выбор кандидата с метриками/причиной, окно кольца, timestamp-дельты/lag/rates, сводки очередей и циклов проб.
- TRACE: отдельные блоки, протокольные команды и важные ветви при включённой подробной диагностике.
- WARN/ERROR: отказные и нештатные ветки с контекстом, включая подпись, эпоху, LOOP, EXPIRED, память, аренду и транспорт.
Указываются группа/источник/эпоха, peer, handle/subblock, индекс, состояние, причина и значимые величины.
При перегрузке — A/D/X/Q, возраст, длительность подтверждения, g, rates выбранных выдач и требуемое сокращение.
При timestamp-разрыве — предыдущий/новый индекс, timestamp и причина сброса базы.
Ключи и содержимое данных не выводятся; частые события агрегируются с полными счётчиками.
Не выдавать принятие в очередь за доставку, относительный lag за абсолютную задержку, старую опцию за онлайн-подписку.
15. Реализация и проверки
Порядок:
1) Подписки/экземпляры, member-опции и BGP-фильтр, дескриптор, FEC/подпись/timestamp, кольцо.
2) Raw-протокол, NCD + reverse, выдачи с индекса/конца, аренда и защита от повторов.
3) Общая очередь/backpressure, относительные метрики, rates и разгрузка.
4) Стартовые 32 пробы/последующий цикл, выбор путей, защита от циклов и перенос.
5) API непрозрачных данных и потребителя, без интеграции видеокодека в этом этапе.
Проверки:
- Несколько подписок/источников/групп на одном ETCP; stop одной сохраняет остальные и чужое владение.
- Неподписанный узел не создаёт экземпляр, не выполняет crypto/FEC для DATA и не ретранслирует его.
- Источник задаёт различные FEC; decode и проверка одной подписи из k частей, невозможность при k-1.
- Подпись внутри RS; подмена payload/подписи/timestamp/индекса/FEC/эпохи/группы не выдаётся приложению; следующий индекс принимается.
- Части до 1300 байт, чередование малых/полных пачек, максимум k-1 padding; байтовый источник сбрасывает за 100 мс.
- Оборот timestamp, нулевая дельта, постепенное отставание, догон, полная остановка и длинный разрыв без ложного числа оборотов.
- Переполнение 32-МБ кольца, дырки, EXPIRED до отправки, курсор медленного получателя и ограниченность памяти.
- Первая допустимая часть с конца задаёт общий N; остальные выдачи начинают с него. READY требует восстановленной проверенной пачки.
- Идемпотентные запросы/продления/закрытия, водяной знак handles, поздние сообщения, REINIT и новая эпоха источника.
- Reverse/неуспех/отмена; потеря одного линка сохраняет выдачи, DOWN всего NCD запускает восстановление.
- Перекрытие поставщиков без дублирования потребителю, rollback, одновременное восстановление нескольких субпотоков.
- Цикл по устаревшим метрикам, одновременная перестройка и выдача сохранённых путей: DATA не циркулирует.
- Глитч короче подтверждения, устойчивое отставание 500 мс, нулевой выход и локальная перегрузка uasync.
- Выбор числа закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и закрытие остатка через 2 секунды.
- 32 совместимых кандидата на старте, далее 1/с, маленький/большой список и запрет повторного полного цикла раньше 30 с.
- Merkle-опции до/после BGP-события, офлайн-опция не кандидат, поздний участник, перезапуск и сохранение других полей userinfo.
- Мобильный источник/получатель и исключение мобильных из фоновых проб ретрансляторов.
Перед кодированием фиксируются точная раскладка stream_proto.h, строковая схема member-опций и численные
лимиты собственной очереди/служебной памяти. Это детализация реализации принятых решений, а не повторный выбор транспорта,
подписи, часов, формата содержимого, анонсов, отдельного дозапроса или расписания проб.
Гистерезис обычного переноса и небольшой запас rate сверх g подбираются на стенде с сохранением заданных сроков.
Исходники добавляются в Autotools и применимые desktop/Android CMake-сборки общего ядра.
Тесты добавляются через tests/Makefile.am. Диагностика должна однозначно показывать выбор путей,
пропуски, сбросы timestamp-базы, переключения и фактический результат разгрузки.
16. План работ и текущий статус реализации
Реализация начата 10.10.2026. Работа разбита на проверяемые этапы:
[+] Основа данных в src/stream:
stream_proto.h — канонический формат частей до 1300 байт и подписанной пачки внутри RS.
stream_block — переменный размер RS и одна Ed25519-подпись пачки до кодирования; проверка после восстановления.
stream_buffer — 32 МБ слотов, фактические длины данных/очереди, индекс пачек и вытеснение старейшей пачки.
stream_metrics — относительное отставание, rate, timestamp wrap/reset и минутная история проблем/остановок.
stream_source — приём непрозрачных байтов, немедленная выдача полной пачки, таймер остатка 100 мс и отмена при destroy.
Добавлены категория stream, Autotools-сборка и test_stream_core.
Desktop/Android CMake уже используют GLOB_RECURSE для src/*.c и автоматически включают новые компоненты.
Проверки: подписи и подмены полей, все 28 пар потерь при 6+2, другие FEC-параметры, padding,
дырки/переполнение кольца, timestamp через ноль/длинный перерыв, остановка и отмена таймеров, учёт утечек.
./build.sh --no-gui -j4 и ./check.sh прошли: 143 passed, 0 failed, 0 skipped;
отдельные proxy/full, burst и load также PASS. Лог: /tmp/stream-check.log.
[+] Включение в жизненный цикл чат-сервиса — 11.10.2026:
stream.c/h — контекст в UTUN_INSTANCE, локальные handles публикации/подписки, общий экземпляр и буфер.
DATA/GAP/END/CLOSED доставляются отложенно через один callback экземпляра ядра;
callback может закрывать handles, заимствованные данные живут до его возврата.
stream_member.c/h — канонические опции, проверка подписанного профиля/ключа и членства,
online-кандидаты только из живых BGP-путей этой CHAT-группы; мобильный источник допускается, relay исключается.
chat_core_set_my_profile_options — атомарная замена опций с сохранением имени, аватарки и остальных userinfo.
При старте снимаются оставшиеся после аварии собственные опции; смена эпохи/ключа или утрата членства отменяют подписку.
Удаление группы и остановка CHAT отменяют таймеры/события, отсоединяют handles и освобождают буферы;
detached handle остаётся во владении вызывающего до stream_close, общий UTUN/транспорт сохраняется.
test_stream_service: четыре живых ядра, Merkle-обмен через ETCP, две группы, общий буфер двух владельцев,
BGP withdraw с сохранённой опцией, смена эпохи, потеря своего членства, удаление канала, restart и отмена таймера.
Проверены rollback размера/SQLite, FEC-доставка непрозрачных байтов и закрытие из DATA callback, DATA перед END.
Тест прошёл, счётчик живых аллокаций после destroy вернулся к нулю. Лог: /tmp/stream-service-test-final.log.
Найденная тестом утечка существующих BGP-путей исправлена: записи освобождаются до контейнера node_paths.
Общий ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full, burst и load — PASS.
Лог общего прогона: /tmp/stream-service-check.log (107 с).
[+] Сетевая выдача — 11.10.2026:
stream_wire.c/h — big-endian формат, raw ETCP_ID_STREAM=0x07, routed ETCP_RT_ID_STREAM=0x3C,
INFO/ACCEPT с границами окна и путём, 64-битные эпохи/handles, отдельная подпись источника на END.
stream_network.c/h — собственный NCD owner пира, общий для его потоков/групп; ожидание его UP,
прямые SUBSCRIBE/ACCEPT/REJECT/DATA/GAP/RENEW/UNSUBSCRIBE/CLOSED/END, routed REVERSE_REQ/CLOSED.
Новые handles назначаются при постановке SUBSCRIBE в общую FIFO; watermark защищает от закрытых/старых запросов.
Первый проверенный DATA с конца устанавливает общий N; прочие latest-выдачи ждут его и запрашивают конкретный N.
Повторный stream_receive_restart отменяет upstream старой попытки, оставляя кольцо/downstream;
старые receive handles отсоединяются и требуют close. Уже сохранённый первый блок также устанавливает новый старт.
DATA копируется из кольца только перед отправкой; пути сохраняются по каждому блоку и проверяются на LOOP.
Все выбранные upstream, прошедшие недостающую пачку, позволяют потребителю выдать явный GAP и продолжить.
END подписывает domain/ключ запуска/последний индекс; нельзя завершить выдачу до прохождения её последнего блока/пропуска.
Период RENEW=2с, аренда=6с, первая попытка=5с; reverse не чаще 1/с в этом сроке.
DOWN/REINIT, потеря членства и закрытие локальной подписки отменяют относящиеся выдачи;
закрытие одной группы/выдачи сохраняет чужое владение транспортом и другие группы на том же пиру.
test_stream_service расширен настоящей цепочкой источник -> relay -> получатель поверх ETCP,
общим индексом, новым bootstrap, FEC 6+2 после снятия двух субпотоков, GAP, LOOP, повторами SUBSCRIBE/reverse,
подписанным END, продлениями/таймаутом аренды и асинхронным REINIT.
Две группы с FEC 6+2 и 1+0 используют один ETCP: истечение выдач первой сохраняет DATA второй.
test_stream_core проверяет wire: все 64 бита, усечения, версии, пути/циклы и размеры.
Оба теста прошли под ASAN/LeakSanitizer с detect_leaks=1 и halt_on_error=1, без ошибок и остатка живых аллокаций.
Логи: /tmp/stream-network-asan-core.log и /tmp/stream-network-asan-service.log.
Обычная сборка восстановлена: ./build.sh --no-gui -j4 прошёл.
Общий ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full (9/9), burst и load — PASS.
Лог: /tmp/stream-network-check.log (88 с).
[+] Метрики и разгрузка — 11.10.2026:
stream_load.c/h — баланс A/D/X/Q по packets/bytes для выдачи, пира и модуля;
интервалы >=200мс фактической длительности, история 10с, рост A-D за 1с до искусственных X.
Изменение состава выдач исключает смешанный интервал из роста и сбрасывает его подтверждение.
Кольцо сообщает только фактические вставки/вытеснения; повторный блок не создаёт A.
Списки субпотоков по времени вставки дают точный возраст ещё ожидающих блоков, в том числе вне порядка batch_seq.
Логическая DATA-очередь представлена курсорами и счетчиками: содержимое и история остаются в одном 32-МБ кольце.
Частные/общие метрики lag/rates/late/stops подключены к подписанным DATA; после END ожидание прекращается.
INFO содержит nonce пробы и 64-битный снимок метрик; RTT — uint32 мс с признаком неизвестного.
Условие перегрузки: ожидающая нагрузка старше 200мс и рост либо нулевой D;
непрерывное подтверждение — 500мс по монотонным часам, независимо от количества тиков.
Требуемое сокращение — g + 1% + 1 байт/с, с ограничением суммой доступных rates; при нулевом D — также оценка backlog/2с.
Rates фиксируются при создании OVERLOAD. Расчётное число — подсказка, исполнение проверяется по суммарному rate.
Получатель предпочитает закрытия, сохраняющие k с учётом других поставщиков, затем parity при равных условиях.
OVERLOAD_SELECT предлагает handles; source закрывает только актуальное требование, поздний/повторный выбор безопасен.
Срок 2с начинается с первого запроса и не меняется при повторе. Невыполненный остаток source закрывает сам.
Новые SUBSCRIBE не компенсируют разгрузку: запрет действует до исполнения и 500мс снятого давления;
stream_peer_get_load.accepting сообщает этот статус отдельно от request_active.
Глобальный бюджет включается только для backlog нескольких пиров вместе с CPU/опозданием callbacks;
CPU отдельно не отключает выдачи, уже запрошенное сокращение повторно не учитывается.
Control FIFO ограничена 128 кодограммами по 1094 байта, возрастом 4с и отменой неподтверждённых запросов.
Просроченные ожидающие DATA при активном OVERLOAD учитываются X и пропускаются явным GAP, кольцо не стирается.
Диагностика сообщает A/D/X/Q, age, input/output/g, длительность интервала, cpu/late callbacks,
frozen/released rates, request/deadline и нижние очереди/inflight; D остаётся принятием ETCP, а не доставкой.
test_stream_core: баланс, X отдельно от D, граница 500мс, глитч, смена состава, 10-секундное окно и широкие wire-метрики.
test_stream_service: реальная заблокированная очередь ETCP, короткий глитч без закрытий, частичная parity-разгрузка,
продолжающийся FEC decode, несколько закрытий, подавленный SELECT/fallback/поздний выбор и общий бюджет двух пиров.
Высокий CPU без backlog не создаёт нового OVERLOAD. Оба теста прошли под ASAN/LeakSanitizer без ошибок и утечек.
Логи: /tmp/stream-pressure-asan-core.log и /tmp/stream-pressure-asan-service.log.
Обычная сборка восстановлена; ./check.sh: 144 passed, 0 failed, 0 skipped;
proxy/full (9/9), burst и load — PASS. Лог: /tmp/stream-pressure-check.log (90 с).
[+] Автоматический выбор и перенос поставщиков — 11.10.2026:
stream_optimizer.c/h — один планировщик на локальный экземпляр, общий для всех owners подписки.
stream_subscribe_open автоматически запускает получение; stream_subscribe_manual_open оставляет ручные выдачи для диагностики.
Режимы не смешиваются; внешний receive/probe/restart не может вмешаться в автоматическое владение handles.
INFO-only probe не резервирует SUBSCRIBE handle и не передаёт DATA; одним NCD-владением последовательно опрашивает субпотоки.
Общий stream_peer переиспользует NCD между группами/источниками; CLOSED handle переоткрывается перед новой попыткой.
INFO_REQ запрашивает неизвестную receiver_epoch=0; INFO_RESP подтверждается nonce текущей пробы и собственной эпохой.
Локальный DOWN/REINIT вращает own epoch. При обнаружении новой чужой эпохи own сохраняется,
старые согласованные выдачи отменяются, а незавершённые INFO получают новый nonce: взаимного вращения эпох нет.
Тест односторонней смены поколения подтвердил восстановление без разрыва ETCP и без смены эпохи другой стороны.
Совместимость проверяется в NCD без создания линков: общий исходящий обход IPv4/IPv6/TCP,
возможность reverse по публикуемым адресам и sock_match; фактическую достижимость подтверждает ограниченная проба.
Стартовая серия — до 32 узлов; далее 1 новый узел/с. Посещённые узлы не повторяются в цикле,
новый цикл ждёт завершения обхода/активных проб и минимум 30с; новые кандидаты добавляются без нового burst.
Неуспехи имеют backoff 30/60/120/240/480с; подтверждённая проба восстанавливает доступность.
INFO сохраняет диапазон, фактический путь, metrics, RTT, свободные slots, accepting, queue, CPU и возраст прогресса.
RTT цепочки относится к этому пути; смена фактического пути DATA делает старое измерение неизвестным,
новая проба обновляет RTT соответствующей активной выдачи. Невыбранные снимки истекают через 60с.
Первый поставщик запрашивается с конца; остальные стартуют с его проверенного N, даже если k уже позволили выдать пачку.
Поиск учитывает доступную старую позицию, запрет LOOP и лимит r при наличии альтернатив.
Оценка поставщика в мс: RTT цепочки до поставщика + 2 * RTT до поставщика + 3 * hops + штраф проблем (0..1000 мс).
hops = path.count: путь INFO содержит автора/поставщика, последний переход к получателю добавляет один hop.
Штраф: доля поздних блоков за 60с * 1000 мс + 10 мс при остановках в истории, округление вниз, максимум 1000 мс;
текущая остановка или lag > 500 мс сразу дают 1000 мс. Неизвестные метрики не дают штрафа, неизвестный RTT даёт UINT64_MAX.
Меньшая оценка лучше; при равенстве выбираются меньше общих узлов путей, затем меньший ID поставщика.
Более новый индекс повторно проверяется свежим INFO/SUBSCRIBE: старое объявление last не является вечным верхним пределом live-потока.
Обычно переносится один субпоток; новая выдача перекрывает старую и заменяет её только после проверенного DATA и догоняющего cursor.
По умолчанию: подтверждение улучшения 2с, удержание поставщика 5с, гистерезис оценки 20мс; параметры доступны через stream_selection_configure.
Перенос сравнивает ту же оценку по фактическим метрикам текущей выдачи и INFO альтернативы;
распределение для FEC допускает ухудшение оценки не более гистерезиса. Логи выбора/переноса показывают оценку и её составляющие.
Новая выдача без достаточного продвижения отменяется через 5с, старая остаётся. CLOSED/DOWN восстанавливают несколько отсутствующих субпотоков.
При отсутствии decode-прогресса 5с, недостатке работающих субпотоков и наличии более новой истории возможен явный restart с конца;
пропущенный диапазон передаётся потребителю GAP, кольцо/downstream сохраняются. Сам по себе простой источника restart не вызывает.
stream_selection_get_info сообщает пробы/циклы, выдачи, перекрытия, поставщиков, миграции, отказы/restart и фактическую FEC-защиту.
Защита подтверждается всеми n проверенными работающими субпотоками и не более r на поставщика; при недостатке поставщиков помечается ослабленной.
Владельцы публикации/подписки удерживают ACTIVE; запрошенный STANDBY восстанавливается после закрытия последнего экземпляра.
Подписанный END проходит также для источника без единого DATA.
test_stream_core проверяет границы 32/1с/30с, новые записи, долгий/незавершённый обход и отсутствие повторного burst.
test_stream_service: INFO без DATA, пустой источник, общие owners, цикл 30с, 400 непрерывных пачек,
распределение FEC, перенос с перекрытием, остановка relay, реальный CLOSED, сохранение ETCP, END без данных и восстановление STANDBY.
NCD-тест проверяет прямую/reverse-only/несовместимую пару без изменения соединений.
stream core/service, NCD и standby transport прошли ASAN/LeakSanitizer без ошибок и утечек.
Логи: /tmp/stream-optimizer-asan-core.log, /tmp/stream-optimizer-asan-service.log,
/tmp/stream-optimizer-asan-ncd.log и /tmp/stream-optimizer-asan-standby.log.
Односторонняя смена поколения и окончательный service-тест повторно прошли ASAN/LeakSanitizer:
/tmp/stream-generation-asan-service.log. Общий ./check.sh: 144 passed, 0 failed, 0 skipped;
proxy/full (9/9), burst и load — PASS; /tmp/stream-optimizer-check.log (97с).
[ ] Расширенные сетевые проверки:
реальные NAT/direct/reverse, большой набор поставщиков, смена адресов и мультилинк под длительной нагрузкой,
одновременная перестройка нескольких ретрансляторов, нагрузка нескольких автоматических источников/групп и mobile standby на устройстве.
Живой стенд проверяет несколько поставщиков на четырёх ядрах; граница 32 и сроки отдельно проверяются чистой политикой расписания.
17. Зафиксированный сетевой формат и пределы реализации
Все stream-числа на wire — big-endian, без packed-структур и зависимости от host byte order.
Версия stream wire — 4; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=5): cmd:1, version:1, command:1, subblock:1,
group_id:8, source_node_id:8, source_epoch:8, sender_epoch:8, receiver_epoch:8, handle:8.
INFO_REQ/RESP согласуют sender/receiver epochs; receiver_epoch=0 используется INFO_REQ для обнаружения поколения и REVERSE_REQ.
Подписанный routed payload содержит тот же заголовок; его group_id сверяется с реальной группой router,
sender берётся из end-to-end router metadata. DATA через routed service отвергается.
INFO_REQ: probe_nonce:8, не резервирует номер SUBSCRIBE.
INFO_RESP/ACCEPT: probe_nonce:8 (0 в ACCEPT), actual_seq:8, first_available:8, last_available:8, n:1, r:1, block_size:2,
metric_flags:1, lag_ms:8, source_rate:8, receive_rate:8, blocks:8, late_blocks:8, stops:8,
offer_flags:1, free_slots:2, cpu_basis_points:2, queue_bytes:8, progress_age_ms:8, path_rtt_ms:4,
path_count:1, node_ids:8*path_count — фиксированная часть 111 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
metric_flags: bit0 — измерение известно, bit1 — source_rate известен, bit2 — receive_rate известен, bit3 — stopped.
offer_flags: bit0 — принимает новые выдачи, bit1 — RTT цепочки известен, bit2 — возраст прогресса известен,
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока. CPU — 0..10000 (сотые доли процента).
Пустой источник: first/last=UINT64_MAX, bit4=0, INFO actual_seq=UINT64_MAX, ACCEPT actual_seq=0, путь {source}.
Курсор last+1 допустим только в ACCEPT непустого кольца; DATA/END проверяются по точной позиции и подписи источника.
SUBSCRIBE: requested_seq:8; REJECT: reason:1, для SUBSCRIBE дополнительно first_available:8,last_available:8,
для INFO дополнительно probe_nonce:8. Старый nonce не подтверждает новую пробу.
RENEW: один header handle без payload (каждая кодограмма продлевает один элемент списка).
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,неизменяемая часть RS-пачки переменной длины.
GAP: old_cursor:8,next_cursor:8, уведомление включает диапазон [old_cursor,next_cursor).
END: "stream_end":10,group_id:8,source_node_id:8,source_epoch:8,last_seq:8,Ed25519:64 — всего 106 байт.
UINT64_MAX в END означает пустой источник; принимается только выдачей с next=0 и без ранее принятых DATA.
OVERLOAD: request_id:8, required_rate:8 (байт/с), estimated_count:1, candidate_count:1,
{handle:8,frozen_rate:8}*candidate_count; не более 64 элементов.
OVERLOAD_SELECT: request_id:8, selected_count:1, handles:8*selected_count; не более 64 элементов.
Оба сообщения идут по raw ETCP с проверенными эпохами пира и действуют только на существующие выдачи.
Список проверяется полностью до действия; повторный/поздний id не создаёт новое требование и не продлевает срок.
Предел пути — 32 узла. Сохраняются только реальные узлы пути; исходный {source} не требует отдельной аллокации.
Память пути отделена от 32-МБ бюджета данных и ограничена числом блоков кольца:
до (8 + 8*32) байт на блок плюс поля индекса и overhead allocator; источнику эта память путей не нужна.
64 stream-пира, 256 активных выдач всего и 64 на пира. Control FIFO — до 128 кодограмм,
каждая не больше 1094 байт wire плюс 8 байт локального времени постановки; DATA в ней не хранится. На пира один waiter send_input_q,
отправка по освобождению до пустого порога; курсоры downstream обслуживаются round-robin.
Для временного индекса каждый фактический блок дополнительно имеет seq/received_ms и два uint32 указателя списка субпотока.
Логический Q не выделяет память под копии/задания на весь backlog и ограничен доступными блоками колец и числом выдач.
Эти пределы общие для ручных выдач, INFO-only проб и автоматических перекрытий; исчерпание не обходится планировщиком.
Снимки кандидатов — отдельно от DATA-кольца, по n субпотоков только у опрошенных узлов;
невыбранные снимки освобождаются через 60с (либо после действующего backoff), не создают дополнительных DATA-буферов.
18. Стенд с реальной сетью Linux
Отдельный каталог tests/stream_netns; запуск вручную через make -C tests check-stream-netns.
16 процессов ядра/CHAT, по одному на veth в своём network namespace; 10 узлов за шестью NAT-шлюзами.
Три сегмента стран: RTT 50/100/150мс, jitter 2/5/10мс, loss 0.2/0.5/1% в одну сторону;
общая полоса межстрановых каналов 50/30/20Мбит/с. Внутри страны RTT около 2мс.
Задержки/потери — receiver ingress IFB/netem, полоса — WAN TBF; очереди ограничены, offload veth отключён.
Cone (EIM/EIF), restricted (EIM/APDF) и строгий NAT (ADM по IP назначения/APDF) проверяются реальными UDP-пробами до старта ядра.
После проб сеть создаётся заново: рабочий запуск начинает с пустыми NAT mappings/conntrack и исходными netem seeds.
Главный namespace не настраивается: новые user/mount/PID/network namespace, без uplink наружу и без именованных ip netns;
снимки адресов/маршрутов/rules/qdisc до и после сравниваются, естественный ход DHCP/cache timers исключается.
Членство создаётся штатным JOIN; источник/подписки распространяются штатным Merkle.
Перед стартом проверяются BGP-пути и подписанные профили всех участников, включая собственный профиль узла.
Нагрузка — opaque пачки 6+2 с sequence, ID источника, общей monotonic меткой времени и SHA256.
Проверяются DATA после FEC, порядок, явные GAP, конечный cursor, END, 32-МБ предел кольца и прекращение DATA после close подписки.
Baseline по умолчанию 120с реального времени; наблюдается оборот 16-битного source timestamp.
Дополнительный сценарий: уменьшение A-C до 2Мбит/с, RTT 300мс, loss 5%, восстановление и отключение двух подписчиков.
Источник можно выбрать за NAT; UDP, UDP+TCP и принудительный TCP доступны как отдельные режимы.
Снимки сохраняют пути субпотоков, rates/lag/stops, буферы, пробы/миграции и внешнюю one-way latency;
логи каждого процесса, команды network/tc/nft и PASS/FAIL с причиной находятся в каталоге запуска.
END, принятый от первого поставщика, сообщает границу источника, но не завершение локального потребителя.
Оптимизатор продолжает выбор/восстановление до consumer cursor > last_seq (либо подписанного пустого END).
Это проверяет регрессия test_stream_service: ранний END при неполной FEC-пачке не блокирует последующую DATA и END приложения.
Restart в той же source_epoch сохраняет consumer cursor: bootstrap от отстающего поставщика не повторяет уже выданные DATA.
Повтор restart до отложенного GAP сохраняет предыдущую, ещё не сообщённую приложению границу; пропуск сообщается одним GAP.
Инструкция и параметры стенда: tests/stream_netns/README.md.
8. Live media и Android
Транспортный wire v5 не меняется. stream_publish_block публикует одну непустую FEC-пачку
немедленно, с сохранением границ. Первая запись выбирает режим источника: byte write или block write;
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
Медиаблок uAV2 находится целиком внутри входа RS, поэтому header восстанавливается вместе с данными.
При штатных 6+2 полный медиаблок <=7345 байт. Раскладка big-endian, без packed-структур:
fixed header (32 байта):
magic="uAV2":4, video_codec:1, audio_codec:1, video_count:1, audio_count:1,
width:2, height:2, rotation:2, header_size:2, video_size:4, audio_size:4,
first_video_frame_offset:4, config_revision:4 (ненулевой, возрастает при пересоздании видеозахвата).
Далее video_count описаний по 32 байта, audio_count описаний по 16 байт,
затем video_size видеобайтов и audio_size аудиобайтов. Описания задают последовательные
неперекрывающиеся диапазоны внутри соответствующего трека; сумма длин должна совпадать с header.
video descriptor:
frame_id:8, PTS_us:8, frame_size:4, fragment_offset:4, fragment_length:4, flags:1, reserved=0:3.
audio descriptor:
PTS_us:8, packet_length:4, samples:2, reserved=0:2.
first_video_frame_offset относительно начала video bytes: UINT32_MAX — начала нет,
иначе это смещение первого фрагмента с fragment_offset=0. Начал может быть несколько,
таблица описывает каждое. Данные конфигурации не являются отдельным кадром.
KEY=bit0 video flags; остальные биты запрещены. frame_id>0, <=16 описаний каждого трека.
Один полный видеокадр с конфигурацией <=2MiB. Общий формат допускает медиаблок <=64KiB; реальный максимум дополнительно
ограничен входом RS. Аудиокодек фиксирован на source_epoch. Кодек, разрешение, поворот и конфигурация видео меняются по config_revision.
Новая revision может пропускать номера; регрессия revision во входящем потоке отвергается.
Изменение metadata без новой revision запрещено. При переходе каждый media cursor удаляет
неполный видеокадр и допускает новую revision только с полным KEY, сохраняя аудио.
Отстающий reader может дочитать прежнюю revision из кольца: сравнение ведётся с revision его выданного видео.
video_codec: 1=H.264, 2=H.265, 3=VP9, 4=AV1, 5=H.266/VVC. B-кадры при захвате запрещены.
KEY payload: config_length:4BE, decoder configuration (<=64KiB), access unit (непустой).
Обычный payload содержит только access unit. frame_size/offset учитывают всю оболочку KEY.
Конфигурация повторяется с каждым KEY, защищена подписью/RS и разбирается только после полной сборки кадра.
H.264/H.265/H.266 используют Annex B; VP9/AV1 сохраняют собственные access units и opaque codec configuration.
Для H.264 конфигурация содержит SPS/PPS, для H.265 — VPS/SPS/PPS; самодостаточный KEY может иметь пустую конфигурацию.
Ранняя подписка и возобновление после GAP не требуют отдельного CONFIG-запроса. Старый uAV1 отвергается.
audio_codec=0: нет аудио; audio_codec=1: Opus mono, audio_codec=2: Opus stereo.
Оба формата: 48000Hz, 960 кадров на канал (20ms), packet<=1275 bytes; interleaved stereo содержит 1920 отсчётов PCM.
Opus-пакеты между медиаблоками не разрезаются. Размеры треков не включают header/описания.
stream_media проверяет весь блок до изменения view. Store индексирует подтверждённые пачки,
а отдельный cursor каждого reader собирает один последовательный кадр прямо из общего RS-кольца.
При GAP, потере продолжения или EXPIRED reader сбрасывает сборку и начинает с полного KEY.
Полный KEY передаётся декодеру сразу, без таймера ожидания; частичный KEY и зависимые кадры до него не выдаются.
Потери, восстановленные и проверенные FEC, не создают GAP и не требуют перезапуска декодера.
Аудио читается независимо; неверный формат/длины не передаются платформенным декодерам.
Конфликт metadata завершает чтение ошибкой. Неиспользуемый отдельный stream_media_receiver удалён:
проверки сборки и восстановления выполняются на рабочем store/cursor.
Desktop и Android открывают просмотр через stream_media_open: ядро проверяет выбранный source_epoch
и эпоху существующей session, создаёт подписку и cursor; при ошибке снимает только нового владельца.
Платформы сохраняют собственное планирование чтения и вывод; закрытие использует обычный stream_close.
Ретрансляторы не разбирают uAV2 и не пересоздают подписи источника.
Android:
- Один 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;
видны только поддержанные encoder/profile/Surface/size/FPS сочетания, аппаратные encoder имеют приоритет.
Режимы камеры <=1920x1080 пересекаются с возможностями выбранного encoder. Обычные 30/60fps требуют AE и minFrameDuration;
скоростные 120/240fps требуют фиксированного диапазона getHighSpeedVideoFpsRangesFor, скоростной сессии,
createHighSpeedRequestList и setRepeatingBurst. Битрейт зависит от размера/FPS, ограничен 16Mbit/s и codec caps.
I-frame interval=1s. Выход нормализуется в Annex B;
partial output собирается в access unit. Codec config передаётся с каждым KEY.
- Битрейт выбирается отдельно: Авто по разрешению/FPS либо пресет в диапазоне encoder.
На LIVE применяется PARAMETER_KEY_VIDEO_BITRATE; при отказе используется штатная смена конфигурации/rollback.
Смена кодека/камеры/режима создаёт revision и принимается вместе с KEY, без смены source_epoch и перезапуска звука.
Encoder configuration из отдельных NAL callbacks объединяется без дублирования parameter sets.
Разделённые CODEC_CONFIG buffers собираются отдельно с пределом 64KiB до разбора конфигурации.
- Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему
origin System.nanoTime; время callback/получения блока не заменяет capture PTS.
- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps
применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate,
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.
stream_publisher держит pthread mutex, ограниченные очереди и один pending post либо media timer;
статистика обслуживается отдельным таймером 100ms. Тот же модуль используется desktop.
attach/detach выполняются в uasync вне callbacks. detach отменяет post/timer, закрывает handles,
очищает media, снимает наблюдение каталога и запрещает producers до уничтожения instance/ua.
UI не сохраняет borrowed inst/ua. Idle без pending data не будит мобильное ядро.
- TX<=2MiB/128 records, не более 250ms ожидания. Actor публикует <=2 пачек за вызов,
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY.
Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms.
- RX<=4MiB/256 records, bridge готовит один ответ на незавершённый запрос worker.
Получение ответа и сетевые AVAILABLE не запускают автоматическое чтение следующего кадра.
Запас остаётся в RS-кольце. want_video запрашивает следующий полный видеокадр независимо от часов показа;
audio_ahead ограничивает только звук относительно PTS потребителя. Управление/звук обслуживаются независимо от video pressure.
StreamDecodePump держит один рабочий кодированный кадр и один ожидающий output MediaCodec.
Decoder заполняет свой внутренний конвейер до первого выхода без ограничений по числу поданных кадров или video lookahead.
Перед каждым входом проверяется выход; свободный input buffer берётся только для уже готового рабочего кадра.
Будущий output ждёт своего PTS и приостанавливает новые video requests; уже запрошенный ответ помещается в рабочий input.
Низкая задержка запрашивается через KEY_LOW_LATENCY при заявленной поддержке, но корректность не зависит от этого режима.
За проход обслуживаются до четырёх media replies, до восьми output buffers за один вызов pump.
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
Просроченный кадр при нормальной последовательности декодируется без показа.
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
AudioTrack задаёт время показа видео по реально услышанному PCM;
без аудио используются монотонные часы с остановкой на границе готовых кадров.
Защитный guard минутного контроллера учитывает audio device lead и наблюдённое опережение decoder
(максимум last_input_PTS-output_PTS текущей revision плюс период кадра). Drain не обнуляет этот guard;
новый decoder начинает измерение заново. Отдельного контроллера задержки декодера нет.
Отсутствие output progress 10s при наличии работы даёт ERROR; сетевые паузы и ожидание PTS допустимы.
Состояния ожидания KEY и decoder разделены; логи показывают inputs/decoded/shown, рабочие slots, inflight и decoderLeadUs.
Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline.
Локальный preview получает прямой Camera2 output в TextureView одновременно с encoder Surface.
Собственный источник не открывает viewer/cursor, decoder, AudioTrack и playback controller.
Preview ориентируется по сенсору камеры и текущему положению экрана; ручной поворот трансляции его не вращает.
TextureView уже применяет ориентацию сенсора. UI Matrix компенсирует только -displayDegrees,
размеры view учитывают relative rotation; повторно поворачивать на SENSOR_ORIENTATION нельзя.
DisplayListener обновляет preview и метки кнопок также при 180°, без изменения Configuration.
Верх картинки зрителей отмечается красной гранью на кнопках поворота; метка «Верх» на видео не рисуется.
В обычном режиме preview выбирает поддерживаемый SurfaceTexture размер того же aspect, не более 1280x720;
high-speed использует размер encoder и createHighSpeedRequestList для двух targets с фиксированным FPS range.
Attach/detach preview меняет только CameraCaptureSession, сохраняя camera, encoder, audio, revision и origin.
При отказе дополнительного output логируется ERROR, UI показывает ошибку preview, камера возвращается к encoder-only session.
TextureView возвращает false из onSurfaceTextureDestroyed; UI, очередь команд camera thread и camera sessions
держат отдельные ссылки. Surface/texture освобождаются после последнего onClosed/onConfigureFailed и ухода UI.
CameraDevice.onClosed освобождает оставшиеся ссылки всех своих sessions: Android не всегда доставляет
session.onClosed при закрытии всего устройства. Повторный callback не освобождает ссылку второй раз.
Логи direct preview показывают first frame, FPS, возраст frame по SurfaceTexture timestamp и поворот;
для SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME возраст считается через elapsedRealtimeNanos,
при UNKNOWN абсолютный возраст не выдумывается, логируется timestamp; начальная очистка texture с timestamp=0 не считается кадром.
camera session logs показывают generation, outputs, размер preview и сохранённый encoder/origin.
Удалённый viewer применяет video rotation источника. ON_STOP закрывает viewer либо отсоединяет прямой preview;
ON_START создаёт output заново. Lifecycle отключает target сразу, независимо от приостановленной Compose recomposition.
Камера при закрытии preview продолжает работать; publisher от UI не зависит.
Видимый player удерживает экран включённым. Preview ограничен оставшимся местом, кнопки управления не перекрывает.
- Source list общий для Android/desktop: stream_sources_json в uasync использует stream_member_load
и stream_member_online. stream_watch_sources наблюдает один выбранный канал; group=0 снимает наблюдение.
Начальный snapshot и изменения после Merkle/BGP передаются через CHAT_EVT_STREAM_SOURCES=58:
group_id:8BE, valid:1, JSON с id(decimal string), epoch(hex string), self, name.
События объединяются отложенным callback, одинаковый список повторно не отправляется. Таймера опроса нет.
Обнаружение не создаёт подписку, media-буфер и не удерживает ядро в ACTIVE.
- В шапке канала значок виден при наличии источников; один источник открывается сразу,
несколько выбираются из списка по имени/ID. Пункт меню «Трансляции» также открывает управление публикацией.
Выбранная эпоха проверяется actor до подписки и по фактически открытому handle. Перезапуск источника
требует нового выбора. Ошибка viewer kind=6 передаёт stream_close_reason в flags.
- Ручная ориентация доступна до запуска и во время своей трансляции: три компактные кнопки 0/90/270 градусов относительно
обычного положения устройства. Поворот не зависит от текущего положения экрана; SENSOR_ORIENTATION и направление
камеры учитываются при расчёте video rotation. Камера/режим/ориентация хранятся и подтверждаются одной конфигурацией.
При смене камеры выбранная ориентация сохраняется; rollback восстанавливает всю предыдущую конфигурацию.
Активная кнопка подсвечивается. Верх отмечается на самой кнопке с учётом передней/задней камеры и положения экрана.
- Смена камеры/режима/ориентации сохраняет publisher owner, source_epoch, микрофон, origin и frame IDs.
Camera callbacks и codec output отсеиваются по поколению. Новая камера открывается после onClosed предыдущей;
close timeout=3s, KEY/подтверждение режима timeout=8s.
Скоростной режим подтверждается после 3s фактического encode rate >=80% целевой FPS; до этого lastGood не заменяется.
При ошибке — одна попытка восстановления последнего успешного режима.
Новая конфигурация и первый KEY принимаются native bridge атомарно; старая очередь видео удаляется, аудио сохраняется.
JNI pull header 28 bytes: kind:1, flags:1, width:2, height:2, rotation:2, PTS_us:8, length:4, revision:4, codec:1, reserved:3.
Kotlin и desktop viewer пересоздают только видеодекодер при смене revision; audio clock не сбрасывается.
- Stop сначала закрывает capture producers и encoder, затем FIN.
Publisher сохраняет конечный буфер ещё 2s для сетевой выдачи, затем снимает своё владение.
При наличии локального reader завершённый источник и кольцо живут до его close, позволяя дочитать FIN.
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу.
Завершение логирует срок drain, оставшиеся RS-байты выдач и локальных readers; RS=0 не означает подтверждение доставки ETCP.
Поздние команды старого session ID не меняют новый запуск.
- Звонок, рация и voice recording не начинают новый capture во время трансляции;
трансляция также отвергается при активном звонке/рации/voice recording.
Логи: stream — epoch/owners, очереди, GAP и teardown; video — codec/config, KEY, PTS и декодер.
Kotlin progress — DEBUG раз в секунду; C packet-level диагностика — TRACE по запросу.
Проверки: test_stream_media (все пары потерь 6+2, parser/границы/GAP), test_android_stream
(живое ядро, preview, verified snapshots, FIN/cancel, concurrent producer/detach и leak count),
Android H264AnnexBTest (Annex B/length-prefixed NAL, SPS/PPS/IDR и усечённые данные),
StreamCameraModesTest (точный режим новой камеры, снижение FPS и fallback 720p30),
StreamOrientationTest (четыре положения переднего/заднего сенсора, включая SENSOR_ORIENTATION=0),
StreamPreviewTest (поворот прямого preview по сенсору/экрану и lifetime Surface при смене session/уничтожении UI).
Ручной поворот без изменения размера также проверяется test_android_stream с непрерывным аудио.
C-тесты проверяют новую revision, потерю первого KEY, stale revision, сохранение аудио и source_epoch.
Проверка 2026-10-12 на SM-A525F / LineageOS 23.2-20260622-NIGHTLY-a52q:
переключение передней/задней камеры, 720p/1080p30, выход/возврат на экран и stop работают.
Ручные 0/90/180/270 градусов проверены на SM до запуска и во время трансляции; viewer сохраняет подписку,
ориентация превью совпадает с камерой, метка «Верх» меняется вместе с ручным video rotation. После смены камеры и повторного открытия экрана угол сохраняется.
Проверен rollback отклонённого 120fps режима: прежние 30fps и ручные 270 градусов восстанавливаются вместе.
Заявленные 720p120/240 не прошли подтверждение скорости: при 720p120 задняя камера даёт около 3.8fps,
передняя — около 30fps. Camera HAL при [120,120] на задней камере возвращает SENSOR_FRAME_DURATION=266666666ns.
Проверка нового режима отвергает такую скорость и восстанавливает прежний режим с уведомлением.
Это наблюдение относится к данной прошивке; metadata Camera2 сами по себе не подтверждают рабочую скорость.
Desktop:
- Значок трансляций расположен в шапке MessageList группового канала даже без источников;
меню содержит запуск/управление собственной публикацией и выбор удалённого источника.
Собственный источник открывает управление; микрофон не воспроизводится локально.
Источники берутся из того же каталога ядра;
ответы предыдущего канала не меняют текущую шапку. Личные беседы снимают наблюдение.
- MainWindow владеет одним StreamPublishEngine. Окно StreamPublishWindow можно закрыть и открыть
снова без завершения эфира. Qt >=6.6 Multimedia/MultimediaWidgets и FFmpeg нужны для публикации;
без них сохраняются остальные функции чата. CMake сохраняет признак capture в отдельном marker,
deployment/Windows installer включают Multimedia runtime, FFmpeg plugin и транзитивные DLL.
- Видео: QCamera, QScreenCapture или QWindowCapture передают последний QVideoFrame в QVideoSink.
Под Wayland/XWayland экран выбирается через системный портал. Открытие портала не ограничивается
таймаутом подтверждения KEY до появления первого сырого кадра.
Callback только заменяет последний кадр под mutex, Qt-очередь кадров не растёт.
Сырое превью обновляется отдельно до 30Hz; encode/conversion и сетевой publisher его не задерживают.
Для камеры сохраняется относительная шкала backend PTS с привязкой к монотонному origin;
при отсутствии PTS используется время захвата callback. Это оценка, а не аппаратный timestamp сенсора.
Неподвижный экран кодируется повторно с новым монотонным PTS; последний raw кадр допустим и после
смены видеоконфигурации на backend, который уведомляет только об изменении изображения.
- Отдельный worker преобразует кадры через swscale и кодирует без B-кадров, GOP=1s.
Предлагаются H.264/H.265/VP9/AV1/H.266 в этом порядке, если реальная проба encoder успешна.
NVENC/QSV/AMF/VAAPI проверяются перед программными реализациями. Проба требует два принудительных
независимых KEY, затем encoder открывается заново, без reference frames и PTS пробной сессии.
Encoder, не выполняющий повторный запрос KEY, для live не используется. VAAPI/QSV async_depth=1.
H.264/H.265/H.266 нормализуются в Annex B; каждый KEY содержит decoder configuration.
- Настройки: разрешение/FPS, видеокодек, Авто/пресеты bitrate, три кнопки 0/90/270 с красной верхней
гранью и подсветкой активной кнопки. libx264 меняет bitrate в текущем encoder; остальные реализации
заменяют только video encoder. Смена codec/режима/rotation подтверждается новой revision с KEY.
Capture и encoder имеют отдельные поколения: смена codec/bitrate/rotation не переоткрывает источник
и не вызывает повторный выбор в портале. Origin, source_epoch, аудио и frame IDs сохраняются.
Ошибка или отсутствие KEY в течение 8s вызывает одну попытку rollback всей последней подтверждённой
конфигурации; повторная ошибка завершает эфир. Late callbacks отсеиваются по поколению источника.
- Микрофон выбирается до запуска. miniaudio захватывает 48kHz stereo; при mono вход дублируется
с явным указанием Mono в UI. Отдельный worker использует Opus AUDIO/128kbps/20ms и собственный
audio_compressor с текущими настройками рации (200ms lookback, нулевой lookahead, общий stereo gain).
Callback копирует PCM в предвыделенную очередь; DSP/Opus в нём нет. Hardware PTS используется при
наличии, иначе шкала строится по количеству отсчётов. Очередь/возраст ограничены; при разрыве
сбрасываются только неполный PCM-пакет и история компрессора. Mute передаёт тишину.
Fatal ошибка audio encoder завершает публикацию по session ID, без отката видеонастроек;
причина сохраняется в окне управления после FIN. Поздняя ошибка старой сессии игнорируется.
Публикация эксклюзивно занимает микрофон: звонок/рация/запись и публикация запрещают друг другу новый
capture. Прежнее совместное владение микрофоном голосовыми функциями сохраняется.
- 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.
Удаление engine происходит до SoundManager и ядра; detach отменяет posts/timers и закрывает publisher
в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются.
- Один StreamPlayerWindow на процесс. Он содержит имя источника, состояние подключения, громкость,
полноэкранный режим и закрытие. Выбор нового источника закрывает предыдущего reader.
- transport/stream_bridge владеет одним viewer, готовит один ответ на запрос и ограничивает RX 4MiB/16 записей.
Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex.
Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers.
- stream_media_cursor собирает ближайший access unit выбранного кодека и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца.
Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus.
FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры.
Worker держит один рабочий AVPacket и не более двух готовых изображений. Следующий video request не ограничен часами показа.
FFmpeg output обслуживается перед input; EAGAIN сохраняет рабочий packet и означает backpressure, не GAP.
Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM.
Кадры сохраняются до достижения их PTS аудиочасами; при задержке GUI общий clock останавливается.
Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY.
- Первый decoded frame разрешает движение clock/audio, защитный guard учитывает наблюдённый decoder lead.
FIN отправляет decoder EOS; выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
Фиксированного prebuffer нет: показ следует физическим audio callback timestamps;
без звука — монотонным часам с ограничением готовым PTS. END до KEY завершает пустой reader.
Короткие пропуски аудио заполняются Opus PLC; большой разрыв/pressure сбрасывает timeline.
Закрытие окна снимает только reader, собственная публикация сохраняется.
Удаление канала сообщает STREAM_CLOSE_GROUP; остановка ядра снимает media и catalogue owners.
- Без FFmpeg окно сообщает, что декодирование недоступно. Нужна desktop-сборка с HAVE_FFMPEG.
Проверки: test_stream_view — настоящее ядро, старые эпохи/cancel, задержанное чтение без роста RX, H.264 с поворотом и сменой revision без новой эпохи,
Opus/громкость на настоящем null audio backend, непрерывные 30fps при физической audio latency 200ms,
закрытие канала и полный баланс tracked allocations. Раз в секунду desktop viewer логирует decode/show FPS,
late/pressure drops, число и байты кадров, PTS границ очереди относительно часов и audio callback timing.
test_stream_service также проверяет общий каталог двух независимых источников одного канала,
шестисекундную паузу readers при живом сетевом приёме без restart и полное локальное/удалённое дочитывание FIN.
StreamDecodePumpTest проверяет конвейеры 4/8 кадров при неподвижных часах без low-latency, ожидание PTS,
блокировку входа, сетевую паузу, пересечение ответа чтения с будущим output, короткий FIN и сохранение guard.
test_stream_playback проверяет восемь будущих кадров из кольца без video lookahead; test_android_stream — один ответ
при повторных запросах, отсутствие скрытого prefetch и возобновление с прежней позиции.
test_stream_view дополнительно задерживает четыре настоящих FFmpeg decoded frames: запуск требует кадра после 80ms,
а короткий поток из трёх кадров получает все изображения после FIN, без потери decoder tail.
test_stream_publish проверяет реальные encoder/decoder H.264/H.265/VP9/AV1, обновление libx264 bitrate
с прежним decoder, общую публикацию/RS, смену revision, pressure/KEY recovery, FIN и concurrent detach
с балансом tracked allocations. Дополнительный Xvfb/private D-Bus сценарий проверяет реальный экран/окно,
стереомикрофон с двумя разными тонами, сырое превью, закрытие окна управления, неподвижный источник,
смену codec/bitrate/rotation и rollback без смены audio/source_epoch, затем отказ Opus после
подтверждённого KEY с завершением эфира и освобождением microphone lease.
Статистика отправки на экране публикации:
- Отдельный uasync timer общего stream_publisher каждые 100ms делает согласованный TX snapshot.
Android получает его через JNI, desktop — через mutex bridge; UI не читает живые очереди транспорта.
Kotlin обновляет отдельный StateFlow, без пересоздания preview. После close/detach timer отменяется.
- video_queue_bytes — оставшиеся байты с вычитанием offset частично упакованного головного кадра.
audio_queue_bytes — целые ожидающие Opus-пакеты. stream_pending_bytes — RS-части всех выдач текущей эпохи,
ещё не принятые ETCP. Это логическая очередь, исходные части продолжают храниться только в кольце.
- transport_queued_bytes — send_input_q, input_queue и остаток packer normalizer.
inflight_bytes — сумма unacked_bytes всех ETCP_CONN общего instance->connections, включая TCP-only/pending.
TCP registry/физические links не обходятся повторно. input_send_q уже включена в unacked_bytes.
- total_bytes = video_queue_bytes + audio_queue_bytes + stream_pending_bytes + transport_queued_bytes + inflight_bytes.
История RS-кольца не является исходящей очередью. Общие ETCP-показатели включают другой трафик узла;
сумма описывает незавершённую отправку в текущем представлении данных, не резерв памяти или полный объём IP-пакетов.
- ACK — байты найденных подтверждённых ETCP-пакетов, не размер пакета ACK. Повтор ACK ничего не добавляет.
Общая per-instance история живёт до destroy ядра, сохраняется при REINIT/close пира и не зависит от наличия BBR link.
Хранится 11 buckets по 100ms: текущий и десять завершённых. ACK_1s — сумма десяти завершённых интервалов;
окно движется каждые 100ms, отсутствующие интервалы равны нулю. Скорость Mbit/s = ACK_1s * 8 / 1000000.
- На экране: остаток видео, общий inflight, total send buffer, ACK_1s в KiB и Mbit/s.
DEBUG stream раз в секунду показывает каждый компонент суммы, ACK и число соединений.
test_stream_tx_stats проверяет границы окна, повторные ACK, закрытие соединений и разделение queued/inflight.
test_android_stream проверяет остаток большого частично упакованного кадра, кодек в JNI и смену revision/epoch.
test_stream_view воспроизводит настоящие H.264/H.265/VP9/AV1, отдельную decoder configuration и FIN.
Общий запас и адаптация воспроизведения:
- Кодированный поток хранится один раз, в stream_buffer (RS-кольцо 32MiB по умолчанию).
stream_media_store хранит только диапазоны seq, PTS готового видео/аудио и позиции KEY.
Индекс ограничен геометрией кольца и 4MiB метаданных; cursor принадлежит отдельному handle.
Live ingest cursor и оптимизатор выдач продолжают продвигаться, независимо от паузы player.
Рабочая память reader — один видеокадр до 2MiB, один Opus-пакет и два scratch медиаблока.
- Подпись пачки проверяется при первом восстановлении. Индексы использованных RS-частей фиксируются;
повторное чтение использует тот же неизменяемый набор, поэтому поздние части не меняют проверенные данные.
Reader не закрепляет старые части против вытеснения. После выхода за окно кольца он начинает с нового KEY.
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
- Общий C-контроллер хранит 60 секундных минимумов запаса: ready_PTS - playback_PTS.
ready_PTS — меньшая из границ полного видео и звука, для preview без звука — граница видео.
Каждое обновление учитывается в минимуме текущей секунды, включая короткий простой.
История нормализована через correction += wall_elapsed - actually_played_media_elapsed.
Старые минимумы автоматически увеличиваются на длительность паузы и уменьшаются на реальный расход при догоне.
Измерения опираются на PTS фактически услышанного PCM, включая аппаратную очередь и периоды тишины.
- После полной здоровой минуты темп плавно повышается до x1.1; SoundTouch сохраняет тон и стерео.
Ускорение расходует только минимальный запас за минуту сверх безопасной границы (не менее 33ms,
с учётом аппаратной задержки), уже поставленного ускоренного звука и плавного торможения.
При нехватке данных темп возвращается к x1.0, часы ждут, сетевой приём продолжает накапливать запас.
После возобновления отсчёт здоровой минуты начинается заново. Разрыв/GAP/новая эпоха сбрасывают историю.
- Диагностика stream: PTS, ready, текущий/минимальный запас, correction, healthy interval, tempo,
pending gain, wait/resume, generation, причины resync и размеры рабочих очередей. DEBUG — раз в секунду.
test_stream_playback проверяет историю, плавный догон с отложенной аппаратной очередью,
независимые readers, позднюю повреждённую RS-часть, вытеснение, GAP, FIN и большой многопачечный кадр.
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1, сохранение тона/баланса,
отображение аппаратного frame counter в исходный PTS и остановку PTS на тишине.
21. Суперузлы: помощь в раздаче по запросу
Сам старт публикации не запускает приём на суперузлах. Новая подписка сразу запускает INFO-пробы и отдельный
дедлайн 100 мс. Если пригодного ответа ещё нет, запрашивается один суперузел. Первый валидный INFO с разрешённой
выдачей выигрывает: порядок приёма фиксируется локальным счётчиком, независимо от RTT и порядка списка кандидатов.
DATA сначала открывается только у победителя; проверенный первый блок фиксирует общий старт всех остальных субпотоков.
Другие INFO-пробы продолжаются: после фиксации старта недостающие части можно брать у любых подходящих поставщиков,
даже до первого восстановления FEC. Победитель гонки не обязан иметь все k частей. Ненужная помощь отменяется.
Пустой доступный источник не требует помощи, пока он не публикует данные.
Для восстановления работающего потока сохраняются 5 секунд недостатка действующих субпотоков для k=n-r.
Для остановившихся выдач требуется также свидетельство более свежих данных;
пауза источника, отсутствие полной FEC-защиты и недобор всех n сами по себе не запускают помощь.
Выбирается один онлайн-суперузел данной CHAT-группы с допустимой NCD-попыткой; после неуспеха пробуется следующий.
Канальная роль берётся из принятого блока владельца adm_tags.supernode, не из client_type/storage/node_type.
Роль суперузла допускает кандидата и на мобильном устройстве; обычные мобильные ретрансляторы по-прежнему исключены.
RELAY_REQUEST/STATUS/CANCEL идут только через подписанный и зашифрованный ETCP-router.
Routed ID стриминга — ETCP_RT_ID_STREAM=0x3C; 0x39 принадлежит MEMBER_FILES и не используется стримингом.
DATA остаётся raw ETCP. Управление помощью обрабатывается до local subscription check, но после проверки
канального членства отправителя. Отсутствие удалённой stream-опции из-за задержки Merkle не запрещает запрос.
Заголовок обычный, subblock=0, handle=случайный ненулевой nonce запроса.
sender_epoch REQUEST/CANCEL — отдельная случайная эпоха попытки; receiver_epoch — поколение сервиса помощи.
sender_epoch STATUS — случайное поколение сервиса помощи; receiver_epoch — эпоха исходного запроса.
Эти поколения независимы от raw stream peer epochs и не сбрасываются при транспортном REINIT.
REQUEST и CANCEL не имеют payload. Первый REQUEST имеет receiver_epoch=0 и только получает подтверждение
поколения через STATUS. Приём/внутренний owner создаётся после повтора REQUEST с подтверждённым receiver_epoch.
Неизвестный запрос с ненулевым receiver_epoch отвергается: позднее подтверждение после удаления tombstone
не может снова создать приём. Начальный старый REQUEST без поколения также не создаёт owner без нового ответа клиента.
STATUS: state:1,reason:1,remaining_ms:4; для ENDED дополнительно неизменённый подписанный stream END (106 байт).
Состояния: STARTING=1, READY=2, BOUND=3, REJECTED=4, ENDED=5, CANCELLED=6.
ENDED в ответе помощи разрешён только для пустого источника (last_seq=UINT64_MAX) и проверяется ключом источника.
Bootstrap ограничен 15 секундами с первого запроса, повторы срок не продлевают. READY означает восстановление
первой FEC-пачки и наличие реальных блоков с допустимым путём к запросившему; окончательные индекс/путь/FEC
проверяются INFO/ACCEPT/DATA. Переход к READY даёт отдельные 5 секунд для SUBSCRIBE.
READY будит selector и снимает старый backoff данного пира. Подтверждённый поставщик временно допускается
и выборкой кандидатов, и receive_open, независимо от задержки его Merkle-опции.
Callback принятых свойств участника Merkle/BGP помечает roster нужного канала устаревшим. Сверка выполняется
отложенно после проверки членства/эпох; несколько callbacks объединяются. Новый совместимый кандидат запускает
INFO без ожидания секундного расписания, а существующие пробы/цикл и backoff ошибок не сбрасываются.
Если новый буфер ещё заполняется и INFO latest отвергнут с POSITION, повторный обход допускается через 100 мс
в пределах фиксированного окна 5 с; транспортный backoff и обычный обход 30 с сохраняются.
Периодическая сверка roster раз в секунду остаётся резервной.
Первое подтверждение поколения и переходы состояний отправляются отложенно сразу; одинаковые повторы ограничены 1 с.
Объединённая работа управления выполняется после packet/NCD callbacks. Общий таймер обслуживает сроки/метрики,
новые INFO/ACCEPT/READY и первый DATA/восстановленный блок будят управление без ожидания следующего тика.
Все таймеры/отложенные вызовы/queue waiters отменяются до освобождения владельца.
После первой действующей выдачи запрос становится BOUND; дальнейшую потребность подтверждают обычные RENEW/аренда.
Выбранная upstream-выдача CONNECTING/INFO/REQUESTED также удерживает исходящий запрос: подписанная Merkle-опция
помощника может прийти раньше STATUS READY. INFO-пробы не удерживают запрос; переход BOUND требует ACTIVE.
Отмена проигравшего запроса не должна закрывать выбранную выдачу того же помощника до её ACCEPT.
Если помощь не даёт прогресса потребителя 15 секунд и набор по-прежнему недостаточен, её выдачи закрываются,
selector пробует другого суперузла. Запросы/ответы не продлевают аренду raw-выдач.
Состояние закрытого запроса сохраняется 60 секунд. Неуспех имеет такой же cooldown для этого суперузла;
после обычного закрытия успешной помощи новый nonce допускается сразу.
Один внутренний owner/экземпляр на точную эпоху обслуживает всех получателей. У него обычные buffer/FEC/selector,
но нет player и пользовательских callbacks, пока нет публичного viewer. Добавление viewer делит тот же буфер.
Режим уже существующего ручного экземпляра сохраняется; запрос старой эпохи не открывает текущую вместо запрошенной.
Внутренний приём удерживает ACTIVE, освобождение восстанавливает запрошенную активность ядра.
Внутренний owner удерживается, пока есть STARTING/READY запрос или любая downstream-выдача ACTIVE,
включая поздних подписчиков, не отправлявших REQUEST. INFO, upstream, ENDED и CLOSED не учитываются как получатели.
После исчезновения потребности выдерживаются 3 секунды, затем закрывается только внутренний owner.
Публичные viewers и чужое владение общим ETCP сохраняются. После END буфер живёт до завершения оставшихся ACTIVE-выдач.
Аренда downstream — 6 секунд, RENEW — 2 секунды; их существующий контракт для обычных handles не меняется.
Удаление группы/членства, новая эпоха и stop закрывают запросы; снятие supernode-роли закрывает внутреннее владение.
Relay-only экземпляр не запрашивает новую помощь и не выбирает relay-only подписчика поставщиком, кроме источника.
Это предотвращает взаимное удержание двух суперузлов разными субпотоками после ухода всех зрителей.
Цепочки исключительно вспомогательных ретрансляторов не строятся; обычные подписчики, включая суперузлы с viewers, допускаются.
Максимум 8 внутренних экземпляров помощи и 128 записей запросов вместе с tombstones на ядро.
Память каждого кольца ограничена 32 МБ; пути/метрики имеют обычные дополнительные пределы stream.
Отказы по роли, эпохе, лимитам, таймауту и перегрузке диагностируются с причиной.
test_stream_service проверяет отсутствие помощи при прямом успехе/паузе источника, автоматический fallback,
дедлайн 100 мс, обе стороны гонки, поздние INFO/REQUEST проигравшего, ограничение ответов на повторы REQUEST и общий старт при n=8/r=2,
выбор помощника через Merkle/INFO раньше задержанного READY без преждевременной отмены выбранной выдачи,
callback обнаружения поставщика без секундной паузы, повторные уведомления без лишних проб и заполнение раннего буфера,
раздачу нескольким зрителям, общий owner с локальным viewer, остановку при уходе последнего,
поздний повтор запроса до/после удаления tombstone, CANCEL до REQUEST, истечение READY без SUBSCRIBE,
откат неуспешного добавления viewer, исключение relay-only кандидатов и передачу пустого подписанного END.