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.
1012 lines
139 KiB
1012 lines
139 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 v3 не меняется. stream_publish_block публикует одну непустую FEC-пачку |
|
немедленно, с сохранением границ. Первая запись выбирает режим источника: byte write или block write; |
|
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника. |
|
|
|
Медиаблок uAV1 находится целиком внутри входа RS, поэтому header восстанавливается вместе с данными. |
|
При штатных 6+2 полный медиаблок <=7345 байт. Раскладка big-endian, без packed-структур: |
|
|
|
fixed header (32 байта): |
|
magic="uAV1":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 описаний каждого трека. |
|
Один H.264 кадр <=2MiB. Общий формат допускает медиаблок <=64KiB; реальный максимум дополнительно |
|
ограничен входом RS. Аудиокодек фиксирован на source_epoch. Разрешение, поворот и SPS/PPS видео меняются по config_revision. |
|
Новая revision может пропускать номера; старая игнорируется. Изменение metadata без новой revision запрещено. |
|
При переходе receiver удаляет неполный видеокадр и ждёт полный KEY, сохраняя историю PTS/кадров и аудио. |
|
|
|
video_codec=1: AVC/H.264 Annex B, без B-кадров. Каждый KEY содержит SPS, PPS и IDR в одном |
|
логическом кадре; это позволяет позднему viewer настроить декодер после получения полного KEY. |
|
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. Receiver собирает один последовательный кадр. |
|
При GAP, потере продолжения или конфликте metadata недособранный кадр удаляется, видео ждёт KEY. |
|
Аудио обрабатывается независимо; неверный формат/длины не передаются платформенным декодерам. |
|
Ретрансляторы не разбирают uAV1 и не пересоздают подписи источника. |
|
|
|
Android: |
|
- Один foreground publisher на процесс, один viewer. Каждая операция имеет новый session ID. |
|
- StreamService владеет камерой, микрофоном и PARTIAL_WAKE_LOCK; пересоздание Activity не завершает источник. |
|
Сервис camera|microphone запускается из видимого приложения после runtime-разрешений; без микрофона — camera. |
|
- Camera2 TEMPLATE_RECORD пишет в Surface AVC encoder. Режимы камеры <=1920x1080 пересекаются с возможностями |
|
конкретного AVC baseline Surface 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 передаётся с каждым IDR. |
|
- 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 держит pthread mutex, ограниченные очереди и один pending post либо timer. |
|
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, подготовлено не более двух media records. Запас остаётся в общем RS-кольце; |
|
bridge читает ближайшие кадры по запросу worker с текущим PTS и отдельными audio/video lookahead. |
|
Kotlin держит до четырёх входных кадров, до трёх поданных decoder PTS и один ожидающий выход. |
|
За проход подаётся до четырёх кадров, выход декодера обслуживается до восьми раз. |
|
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY. |
|
Просроченный кадр при нормальной последовательности декодируется без показа. |
|
- Фиксированного prebuffer нет. AudioTrack задаёт время показа видео по реально услышанному PCM; |
|
без аудио используются монотонные часы с остановкой на границе готовых кадров. |
|
Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline. |
|
Локальный preview использует обычную подписку на собственный источник с отключённым воспроизведением |
|
своего микрофона, чтобы исключить акустическую обратную связь. Локальный preview ориентируется по сенсору своей камеры |
|
и текущему положению экрана; верх картинки зрителей отмечается красной гранью на кнопках поворота. |
|
Положение камеры хранится per-revision отдельно от wire, чтобы queued кадры не использовали параметры новой камеры. |
|
Ручной поворот сохраняет размеры и ориентацию локального превью; метка «Верх» на видео не рисуется. |
|
Удалённый viewer применяет video rotation источника без локальной метки. Камера при закрытии preview продолжает работать. |
|
Viewer закрывается на Activity ON_STOP и создаётся заново на ON_START; 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 24 bytes: kind:1, flags:1, width:2, height:2, rotation:2, PTS_us:8, length:4, revision:4. |
|
Kotlin и desktop viewer пересоздают только видеодекодер при смене revision; audio clock не сбрасывается. |
|
- Stop сначала закрывает capture producers и encoder, затем FIN. |
|
Publisher сохраняет конечный буфер ещё 2s для сетевой выдачи, затем снимает своё владение. |
|
При наличии локального reader завершённый источник и кольцо живут до его close, позволяя дочитать FIN. |
|
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу. |
|
Поздние команды старого 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 (независимое превью, верх передней/задней камеры, экран и camera switch при queued frames). |
|
Ручной поворот без изменения размера также проверяется 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. Источники берутся из того же каталога ядра; |
|
ответы предыдущего канала не меняют текущую шапку. Личные беседы снимают наблюдение. |
|
- Один StreamPlayerWindow на процесс. Он содержит имя источника, состояние подключения, громкость, |
|
полноэкранный режим и закрытие. Выбор нового источника закрывает предыдущего reader. |
|
- transport/stream_bridge владеет одним viewer, готовит до двух media records и ограничивает RX 4MiB/16 записей. |
|
Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex. |
|
Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers. |
|
- stream_media_cursor собирает ближайший H.264 access unit и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца. |
|
Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus. |
|
FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры. |
|
Worker запрашивает видео при наличии менее двух готовых изображений; защитный предел — четыре. |
|
Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM. |
|
Кадры сохраняются до достижения их PTS аудиочасами; при задержке GUI общий clock останавливается. |
|
Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY. |
|
- При начальном подключении 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. |
|
|
|
Общий запас и адаптация воспроизведения: |
|
- Кодированный поток хранится один раз, в stream_buffer (RS-кольцо 32MiB по умолчанию). |
|
stream_media_store хранит только диапазоны seq, PTS готового видео/аудио и позиции KEY. |
|
Индекс ограничен геометрией кольца и 4MiB метаданных; cursor принадлежит отдельному handle. |
|
Live ingest cursor и оптимизатор выдач продолжают продвигаться, независимо от паузы player. |
|
Рабочая память reader — один H.264 кадр до 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 сначала открывается только у победителя; проверенный |
|
первый блок фиксирует общий старт всех остальных субпотоков. До первого восстановления FEC выдачи открываются у победителя, |
|
затем действует обычный оптимизатор. Пустой доступный источник не требует помощи, пока он не публикует данные. |
|
Для восстановления работающего потока сохраняются 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.
|
|
|