Record completed stream header audit and align native guide

This commit is contained in:
evgeny committed 2026-10-26 12:53:40 +01:00
1 parent de535e1713
commit 409d9f3ca9
2 files changed
+109 -28

No files matched your search

+47 -27
View File
@@ -1,12 +1,15 @@
Модуль стриминга. Техническое задание, этап 1.
Статус аудита 26.10.2026: сверка завершена для stream.h, stream_proto.h,
stream_block.h, stream_source.h, stream_media.h и stream_optimizer.h; остальные разделы проверены частично.
Статус аудита 26.10.2026: сверены все 18 заголовков src/stream с полными
реализациями (15 C и playback_audio.cpp). Уточнены API, поля, владение,
сроки и результаты операций; последние правки заголовков не меняют C-токены.
Настоящий документ сверён в части общего ядра stream; платформенные разделы
desktop/Android и подробности внешнего сетевого стенда остаются частично проверенными.
Расхождение допуска deleted устранено: stream_member_load читает принятый флаг
в том же SELECT, что и профиль, и возвращает STREAM_ERR_NOT_MEMBER до crypto.
Штатный test_stream_member проверяет подписи/версии, source/supplier/self,
закрытие handles/выдач, изоляцию каналов и повторное разрешение участия.
Полный аудит оставшихся разделов ещё не завершён. Подтверждения, архитектура
Полный аудит платформенных разделов ещё не завершён. Подтверждения, архитектура
исправления и результаты проверок — doc/stream_audit_findings.txt.
Дефект publisher_block устранён: помещающийся Opus уходит в отдельную аудиопачку,
ожидающее видео получает следующий слот actor. Штатный test_stream_publisher
@@ -27,7 +30,7 @@ stream_block.h, stream_source.h, stream_media.h и stream_optimizer.h; оста
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
- Передаём произвольные данные как есть, без обработки форматов видео, кадров и ключевых точек.
- Транспортная часть передаёт произвольные данные как есть. Необязательный media-слой поверх неё разбирает uAV2 и точки KEY.
- Размер части автоматически уменьшается по длине пачки, максимум по умолчанию 1300 байт.
Побайтовый источник сбрасывает остаток через 100 мс; publish_block публикует сразу.
- На каждый локальный экземпляр трансляции — кольцевой буфер 32 МБ для всех её субпотоков вместе.
@@ -127,7 +130,7 @@ Handle получателя, поколение stream-сессии, измен
Невосстановимый пропуск обозначается событием GAP; нельзя молча склеивать данные по обе стороны разрыва или выдавать нули за исходные байты.
Окончательно пропускаем пачку, когда её данных уже нет в окне либо все нужные выдачи прошли её индекс/сообщили пропуск,
а k блоков не собрано. Получение более поздней пачки от одного поставщика само по себе не закрывает ожидание остальных.
Модуль не интерпретирует содержимое и не занимается видео, кодированием, воспроизведением или поиском ключевых кадров.
RS/сетевой слой не интерпретирует содержимое. Media store, publisher и playback — отдельные компоненты поверх него.
4. Подпись источника и timestamp
@@ -256,13 +259,19 @@ ACCEPT содержит handle, фактический начальный инд
Реальное получение блока, а не только ACCEPT, подтверждает пригодность выдачи.
Индекс сразу после последнего доступного блока разрешён: курсор ждёт его появления в том же кольце.
Более далёкий будущий индекс отвергается. Пустой источник допускает выдачу с 0/с конца,
которая ждёт первый блок; первый проверенный DATA по-прежнему устанавливает общий N.
которая ждёт первый блок; первая структурно допустимая DATA-часть по-прежнему устанавливает общий N.
Пустой ретранслятор пути не объявляет. Завершение без данных подтверждается подписанным END с last_seq=UINT64_MAX.
Общий N фиксирует первая структурно допустимая RS-часть, до проверки подписи всей пачки.
INFO/ACCEPT и счётчик принятых частей не являются подтверждением подписи; она проверяется при decode из k частей.
Отказы различают отсутствие подписки/трансляции, эпоху, FEC, позицию, слот, перегрузку, цикл и право доступа.
DATA содержит поколение сессии, handle, неизменяемую часть RS-пачки и изменяемый путь.
Источник подписывает и END с ключом трансляции и индексом последней пачки, чтобы ретранслятор не мог подделать окончание.
Нет продления 6 секунд — отправитель закрывает выдачу и сообщает CLOSED, при необходимости через маршрутизатор группы.
INFO/connect ограничен 5 секундами; готовая INFO-only проба остаётся до явного close.
Control FIFO удаляет сообщения старше 4 секунд. Просроченные INFO_REQ (9 байтов), SUBSCRIBE и ACCEPT
закрывают соответствующую незавершённую выдачу с OVERLOAD; INFO_REQ дополнительно сопоставляется по nonce.
После первого DATA отдельного таймера тишины receive нет: восстановление выполняет selector либо ручной владелец.
Аренда, пробы и timers используют монотонное локальное время. Таймаут аренды очищает ресурсы;
реакция на отсутствие прогресса начинается раньше по метрикам потока.
Потеря отдельного ETCP-линка не закрывает выдачи, пока NCD остаётся UP.
@@ -274,7 +283,7 @@ DOWN закрывает выдачи соответствующей сессии
Начальный запуск:
1) По опциям мемберов и BGP выбрать первый подходящий онлайн-узел, включая сам источник.
2) Запросить один субпоток с N=UINT64_MAX.
3) Получить и проверить первый блок, зафиксировать его source_epoch и batch_seq=N.
3) Принять структурно допустимую часть и зафиксировать batch_seq=N; source_epoch уже выбран по профилю источника.
4) У остальных поставщиков запросить субпотоки этой же эпохи с конкретного N.
5) Собрать достаточный набор блоков одной пачки, затем продолжать непрерывное получение.
@@ -285,7 +294,7 @@ DOWN закрывает выдачи соответствующей сессии
Обычный перенос одного субпотока:
1) Выбрать первую ещё нужную пачку N и поставщика, способного её выдать.
2) Создать новую выдачу, продолжая старую; временное перекрытие также занимает исходящий слот нового поставщика.
3) Получить блоки и подтвердить продвижение нового поставщика.
3) Получить структурно допустимые части и подтвердить продвижение нового поставщика до курсора старого.
4) Закрыть старую выдачу. При отказе новой сохранить старую и отменить неудачную попытку.
Дедупликация — по (stream key, batch_seq, subblock), независимо от handles.
@@ -324,7 +333,9 @@ DATA несёт фактически пройденные узлы; началь
Базу сбрасываем при отмеченном источником разрыве времени, неоднозначной дельте, новой выдаче/эпохе или разрыве последовательности.
Резкое изменение позиции выдачи не считается доказательством улучшения задержки.
Ожидаемый межблочный интервал и rate источника оцениваются по окну его timestamp и объёмам блоков.
Rate источника оценивается по timestamp и объёмам пар последовательных блоков за 60 секунд.
Ожидаемый интервал сначала равен 100 мс; после ненулевой суммы source_delta текущей секундной корзины
используется её среднее по интервалам, не меньше 1 мс. Первый/rebase-блок не даёт парных bytes/time для rate.
Если несколько блоков имеют один timestamp, агрегируем объём до ненулевой дельты, не делим на ноль.
Не используем время формирования всех n блоков одной пачки как n независимых временных интервалов.
При отсутствии новых данных продолжаем измерять отставание от ожидаемого следующего прихода:
@@ -363,8 +374,9 @@ INFO_RESP также сообщает число свободных исходя
Неуспех имеет backoff; одинаковые активные пробы одного пира для разных экземпляров переиспользуются.
Временные NCD handles невыбранных кандидатов освобождаются; рабочие экземпляры удерживают свои handles.
Используются route_connectivity, sock_match/NCD и метрики существующих соединений; второй адресный probe-движок не создаётся.
Прямая доступность — UNKNOWN/REACHABLE/UNREACHABLE с временем замера, а не вечный boolean.
Используются node_conn_direct_compatible и sock_match/NCD; stream не запускает собственные route_connectivity-пробы.
Совместимость означает возможность direct/reverse-попытки по известным сокетам/адресам либо уже работающий транспорт.
Фактическую доступность и RTT подтверждает INFO-обмен, снимки и неуспехи имеют ограниченный срок/backoff.
После проверки доступного субпотока/индекса и отсутствия цикла минимизируем интегральную оценку в миллисекундах:
score_ms = RTT_цепочки_до_поставщика + 2 * RTT_до_поставщика + 3 * hop_count + problem_penalty_ms
@@ -412,7 +424,8 @@ A — поступившая нагрузка, D — передано в ETCP, X
Rate выдачи — её средний вклад в поступающую нагрузку, а не урезанная скорость выхода из перегруженного канала.
Для разгрузки нужно снять суммарный rate больше g, чтобы очередь начала убывать.
Число выдач оценивается по их rates, но закрывать можно только избыточные части сверх k.
Недостаточная полоса без избыточных частей требует пропуска устаревших данных, а не разрушения FEC-набора.
Без безопасных избыточных частей запрос завершается без принудительных закрытий. Возраст сам по себе не разрешает пропуск:
backpressure сохраняет cursor, GAP возникает при фактическом вытеснении/утрате диапазона.
OVERLOAD содержит request_id, расчётное число выдач, требуемое сокращение rate и список handles с фиксированными rates.
Получатель выбирает handles только при сохранении k различных работающих частей и отправляет OVERLOAD_SELECT.
@@ -580,7 +593,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
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.
Первая структурно допустимая DATA-часть с конца устанавливает общий N; прочие latest-выдачи запрашивают конкретный N.
Повторный stream_receive_restart отменяет upstream старой попытки, оставляя кольцо/downstream;
старые receive handles отсоединяются и требуют close. Уже сохранённый первый блок также устанавливает новый старт.
DATA копируется из кольца только перед отправкой; пути сохраняются по каждому блоку и проверяются на LOOP.
@@ -607,7 +620,8 @@ Stop/destroy выполняются по правилам ядра, вне callb
Кольцо сообщает только фактические вставки/вытеснения; повторный блок не создаёт A.
Списки субпотоков по времени вставки дают точный возраст ещё ожидающих блоков, в том числе вне порядка batch_seq.
Логическая DATA-очередь представлена курсорами и счетчиками: содержимое и история остаются в одном 32-МБ кольце.
Частные/общие метрики lag/rates/late/stops подключены к подписанным DATA; после END ожидание прекращается.
Метрики lag/rates/late/stops обновляются при приёме структурно допустимых частей, до проверки общей подписи;
они описывают доставку поставщика и не подтверждают содержимое источника. После END ожидание прекращается.
INFO содержит nonce пробы и 64-битный снимок метрик; RTT — uint32 мс с признаком неизвестного.
Условие перегрузки: ожидающая нагрузка старше 200мс и рост либо нулевой D;
непрерывное подтверждение — 500мс по монотонным часам, независимо от количества тиков.
@@ -651,7 +665,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
INFO сохраняет диапазон, фактический путь, metrics, RTT, свободные slots, accepting, queue, CPU и возраст прогресса.
RTT цепочки относится к этому пути; смена фактического пути DATA делает старое измерение неизвестным,
новая проба обновляет RTT соответствующей активной выдачи. Невыбранные снимки истекают через 60с.
Первый поставщик запрашивается с конца; остальные стартуют с его проверенного N, даже если k уже позволили выдать пачку.
Первый поставщик запрашивается с конца; остальные стартуют с N его первой принятой части, даже если пачка уже выдана.
Поиск учитывает доступную старую позицию, запрет LOOP и лимит r при наличии альтернатив.
Оценка поставщика в мс: RTT цепочки до поставщика + 2 * RTT до поставщика + 3 * hops + штраф проблем (0..1000 мс).
hops = path.count: путь INFO содержит автора/поставщика, последний переход к получателю добавляет один hop.
@@ -659,15 +673,19 @@ Stop/destroy выполняются по правилам ядра, вне callb
текущая остановка или lag > 500 мс сразу дают 1000 мс. Неизвестные метрики не дают штрафа, неизвестный RTT даёт UINT64_MAX.
Меньшая оценка лучше; при равенстве выбираются меньше общих узлов путей, затем меньший ID поставщика.
Более новый индекс повторно проверяется свежим INFO/SUBSCRIBE: старое объявление last не является вечным верхним пределом live-потока.
Обычно переносится один субпоток; новая выдача перекрывает старую и заменяет её только после проверенного DATA и догоняющего cursor.
Обычно переносится один субпоток; новая выдача заменяет старую после структурно допустимого DATA и догоняющего cursor.
По умолчанию: подтверждение улучшения 2с, удержание поставщика 5с, гистерезис оценки 20мс; параметры доступны через stream_selection_configure.
Перенос сравнивает ту же оценку по фактическим метрикам текущей выдачи и INFO альтернативы;
распределение для FEC допускает ухудшение оценки не более гистерезиса. Логи выбора/переноса показывают оценку и её составляющие.
Новая выдача без достаточного продвижения отменяется через 5с, старая остаётся. CLOSED/DOWN восстанавливают несколько отсутствующих субпотоков.
При отсутствии decode-прогресса 5с, недостатке работающих субпотоков и наличии более новой истории возможен явный restart с конца;
пропущенный диапазон передаётся потребителю GAP, кольцо/downstream сохраняются. Сам по себе простой источника restart не вызывает.
После 5с без полной проверенной пачки, если anchor уже есть либо запрошен media-индекс, выполняется recovery_request;
следующая попытка не раньше чем через 5с. Зависшие выдачи переоткрываются, consumer/media cursors сохраняются.
Для media переход к более новому полному KEY выполняется лишь при доказанной недоступности нужного диапазона;
тогда stream_local_restart сохраняет кольцо/downstream и передаёт пропущенный диапазон как GAP.
Тишина источника может вызвать перезапрос, но сама по себе не доказывает потерю данных и не требует скачка к голове.
stream_selection_get_info сообщает пробы/циклы, выдачи, перекрытия, поставщиков, миграции, отказы/restart и фактическую FEC-защиту.
Защита подтверждается всеми n проверенными работающими субпотоками и не более r на поставщика; при недостатке поставщиков помечается ослабленной.
Защита оценивается по всем n субпотокам с наблюдёнными частями/метриками без stopped и не более r на поставщика;
эта оценка доступности частей не заменяет проверку общей подписи. При недостатке поставщиков защита ослаблена.
Владельцы публикации/подписки удерживают ACTIVE; запрошенный STANDBY восстанавливается после закрытия последнего экземпляра.
Подписанный END проходит также для источника без единого DATA.
test_stream_core проверяет границы 32/1с/30с, новые записи, долгий/незавершённый обход и отсутствие повторного burst.
@@ -689,8 +707,8 @@ Stop/destroy выполняются по правилам ядра, вне callb
17. Зафиксированный сетевой формат и пределы реализации
Все stream-числа на wire — big-endian, без packed-структур и зависимости от host byte order.
Версия stream wire — 4; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=7): cmd:1, version:1, command:1, subblock:1,
Версия stream wire — 7; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=7): etcp_id=0x07: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,
@@ -766,7 +784,7 @@ Restart в той же source_epoch сохраняет consumer cursor: bootstra
8. Live media и Android
Транспортный wire v5 не меняется. stream_publish_block публикует одну непустую FEC-пачку
Текущий транспортный wire — v7. stream_publish_block публикует одну непустую FEC-пачку
немедленно, с сохранением границ. Первая запись выбирает режим источника: byte write или block write;
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
@@ -819,7 +837,8 @@ stream_media проверяет весь блок до изменения view.
Desktop и Android открывают просмотр через stream_media_open: ядро проверяет выбранный source_epoch
и эпоху существующей session, создаёт подписку и cursor; при ошибке снимает только нового владельца.
Платформы сохраняют собственное планирование чтения и вывод; закрытие использует обычный stream_close.
Ретрансляторы не разбирают uAV2 и не пересоздают подписи источника.
Пересылка RS-частей не требует разбора uAV2 и не пересоздаёт подписи источника.
По INFO_REQ.want_media ретранслятор дополнительно готовит общий индекс проверенных пачек для предложения полного KEY.
Android:
- Один foreground publisher на процесс; каждый viewer имеет независимый session ID.
@@ -1074,7 +1093,7 @@ Desktop:
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 не меняются.
в uasync. Общий publisher использует те же uAV2 и текущий wire v7 на обеих платформах.
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну.
- transport/stream_bridge владеет stream_viewers: независимый cursor и ближайший батч на reader.
@@ -1255,7 +1274,7 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
Сам старт публикации не запускает приём на суперузлах. Новая подписка сразу запускает INFO-пробы и отдельный
дедлайн 100 мс. Если пригодного ответа ещё нет, запрашивается один суперузел. Первый валидный INFO с разрешённой
выдачей выигрывает: порядок приёма фиксируется локальным счётчиком, независимо от RTT и порядка списка кандидатов.
DATA сначала открывается только у победителя; проверенный первый блок фиксирует общий старт всех остальных субпотоков.
DATA сначала открывается только у победителя; первая структурно допустимая часть фиксирует общий старт остальных субпотоков.
Другие INFO-пробы продолжаются: после фиксации старта недостающие части можно брать у любых подходящих поставщиков,
даже до первого восстановления FEC. Победитель гонки не обязан иметь все k частей. Ненужная помощь отменяется.
Пустой доступный источник не требует помощи, пока он не публикует данные.
@@ -1449,8 +1468,9 @@ Qt stream_view/audio/publish — 3/3, Android assembleDebug — PASS.
Предыдущий изолированный стенд с 16 узлами: baseline 10с, WAN 2Мбит/с + RTT 300мс + loss 5% на 10с,
восстановление, закрытие двух подписок и END — PASS. Результат: /tmp/stream-recovery-netns/result.json.
Новая реализация использует stream wire v6: обновляются отправитель, получатели и задействованные реле.
Обратная совместимость v5 не поддерживается. Desktop и Android собраны из utun2.
Историческая проверка реализации c3cf86b9 относилась к wire v6; затем телеметрия повысила версию до v7.
При обновлении нужны отправитель, получатели и задействованные реле одной версии; совместимости с v5/v6 нет.
Указанные ниже старые результаты desktop/Android получены из utun2 и не заменяют текущую платформенную проверку.
Проверка реализации c3cf86b9: ./check.sh — 187/187, proxy/burst/load — PASS; Android assembleDebug — PASS.
Девять сетевых регрессий входят в test_stream_service; лог полного прогона: /tmp/utun2-key-check-final-pass.log.
+62 -1
View File
@@ -1,4 +1,4 @@
Аудит документации src/stream — 26.10.2026, частично завершён
Аудит документации src/stream — 26.10.2026, заголовки завершены
Текущий статус: deleted исправлен и покрыт штатным test_stream_member; сборка,
профиль7/7 и check.sh195/195 PASS относятся к внедрению этого исправления (раздел 5).
@@ -11,6 +11,10 @@
заголовки; теперь6[V]/12[~]/0[ ]. В частично прочитанном network подтверждён
дефект истечения INFO_REQ (раздел8). Исправление и штатные проверки выполнены:
TTL4/4, обе реальные backpressure-сцены и check201/201 в отдельном IPv6 namespace PASS (раздел9).
Текущий итог продолжения:18[V]/0[~]/0[ ] для заголовков src/stream; полностью
прочитаны все16 реализаций. Раздел10 описывает финальную сверку контрактов.
doc/stream.txt остаётся[~] из-за неполной проверки платформ desktop/Android
и внешнего многопроцессного стенда. Старые показатели выше и в разделах1–9 — история отдельных этапов.
1. Подтверждённый дефект: deleted не учитывается stream_member_load
@@ -448,3 +452,60 @@ Proxy9/9,burst PASS,load8/8,134217728 байт; полное время175с.
git diff --check PASS. Sanitizers, Qt GUI и Android на устройстве в этом исправлении
не запускались. Устранение дефекта не расширяет аудит:6[V]/12[~]/0[ ],doc/stream.txt[~].
10. Завершение сверки заголовков общего stream-ядра — 26.10.2026
Дочитан stream_network.c1616–2185: relay lifecycle, dispatch, общий tick,
перегрузка нескольких пиров, учёт кольца, позиции получателей и телеметрия.
Вместе с предыдущими этапами полностью прочитаны все15 C-реализаций и
stream_playback_audio.cpp (всего7775 строк), все18 заголовков.
Для уточнения контрактов повторно прочитаны полные buffer/media_buffer,
member/wire, load/metrics, playback_clock/audio, publisher/viewer.
Уточнения заголовков
- network/priv: принадлежность caller-owned handles после detach, manual/internal
ограничения, INFO nonce/повторы/сроки, аренда6с и RENEW2с, control TTL>4с,
пределы общих бюджетов, готовая INFO-проба без автоматического close,
первый DATA как структурная часть до проверки подписи, диапазоны/RTT/validity,
restart с сохранением consumer floor, relay READY/BOUND и сроки помощи.
- load/metrics: A/D/X/Q, атомарность add/remove, revision и окна роста/rates,
live-счётчики против последнего снимка rates, rebase и 16-битный timestamp,
первая пара для rate, строгий порог остановки, END и старение истории.
- buffer/media_buffer: размер слотов против фактических wire-байтов,
заимствованные данные/observer, поиск частей против проверки подписи,
неизменяемый набор k после decode, неподписанные timing/telemetry,
независимые позиции треков, generation/GAP/revision, status до KEY,
bounded MORE/WAIT, audio lookahead и продолжение последнего кадра после finish.
- member/wire: parse/format против доверенного load, BGP отдельно от членства,
собственный online, роль deleted=0, переключатель audio против codec эпохи,
конкретные размеры/предусловия writers/readers, путь, телеметрия и подпись END.
- playback_clock/audio: состояние регулятора/история и часы, допустимые входы,
producer/consumer и внешняя блокировка, 20-мс stereo output, PLC/разрыв,
needs_input/prepare/finish, hardware frame origin, empty до опустошения устройства.
- publisher/viewer: асинхронное открытие/stop, время занятия session ID,
KEY/revision/admission/TTL, feed как принятие очередью, mute без смены codec,
общие ETCP-счётчики instance, notify под lock/исключительный destroy,
владение батчем, диапазоны demand и нулевые снимки до обновления actor.
Последние изменения этих12 заголовков — комментарии/оформление. Сравнение
лексических C-токенов с сохранённой до правок базой проверяет имена, типы,
порядок полей, объявления и выражения; все12 совпали. Публичный API/ABI,
состояния, протокол и C/C++ логика в данном продолжении не менялись.
Новых подтверждённых дефектов реализации в этой части аудита не найдено.
В stream.txt согласованы native-разделы: текущая версия wire7, INFO_REQ9,
сроки и закрытие control, совместимость через NCD без собственного
route_connectivity probe-движка, структурная часть против подписанной пачки,
60-секундные rates, recovery без сброса media и отсутствие пропуска по одному
возрасту DATA. Уточнено, что relay может готовить media-индекс по want_media.
Старые результаты wire6/utun2 помечены историческими.
Покрытие:18[V]/0[~]/0[ ] в src/stream. doc/stream.txt[~]: ещё нужны
полная сверка desktop/Android capture/decoder/audio/UI и внешнего
tests/stream_netns, их аппаратные и старые ASAN-результаты здесь не перепроверены.
Завершение заголовков не объявляет эти внешние компоненты проверенными.
Проверка ./build.sh --no-gui -j4 PASS: /tmp/stream-header-audit-build.log.
Полный ./check.sh запущен в отдельном network namespace с IPv6/lo/::1,
от user; основной namespace не меняется. Результат будет записан после завершения.