Files
cuframes-composer/docs/LESSONS-audio-mixing-attempts.md
gx fa6ab3069a Phase 7 audio mixing — attempt + rollback + lessons
Несколько сессий попыток реализовать audio mixing в композитор'е.
Не достигнуто sub-секундной latency со стабильным video+audio.
Откатано на parallel mode (cfc-grid video-only, live от pipeline с audio).

Полный набор выводов и pitfall'ов — docs/LESSONS-audio-mixing-attempts.md.

Главные lesson'ы для будущей попытки:
- mpegts mux libavformat авто-инсёртит h264_mp4toannexb BSF которому
  не нравится Annex-B + inline SPS/PPS — NVENC OUTPUT_SPSPPS per-frame ломает
- SPSC ring drop newest при full, не oldest (consumer's domain)
- av_new_packet (не av_malloc) для av_interleaved_write_frame ownership
- Monotonic PTS на counter (frame_idx, total_samples) — не wallclock
- mediamtx env-var path names не должны иметь '-' (parser limitation)
- Default mediamtx ReadTimeout=10s короткий для burst write'ов

Изменения в repo сохранены для будущей доработки:
- src/writer.c — mpegts backend с audio stream support
- src/audio.c — RTSP AAC consumer + lock-free SPSC ring
- include/cuframes_composer/{writer,audio}.h — public API
- examples/grid_record.c — --format=mpegts + --audio-source flags
- include/cuframes_composer/composer.h — consumer_prefix field
- docker/Dockerfile — libavformat-dev добавлен в builder/runtime

cfc-grid composer стабильно работает на видео (substantially лучше
монолитного pipeline'а с audio bag'ом). TV рекомендуется использовать
rtsp://...:554/cfc-grid + опционально rtsp://...:554/live-audio
parallel.
2026-06-03 14:29:56 +01:00

18 KiB
Raw Permalink Blame History

LESSONS: попытки добавить audio mixing в cuframes-composer

Контекст: cuframes-composer (gx/cuframes-composer) композирует 4 cuframes-источника в grid через CUDA kernels + NVENC H.264, публикует в mediamtx cfc-grid path (только video). Параллельно cuda-grid-pipeline (legacy монолитный ffmpeg) публикует live с video + AAC audio — но месяц подёргивается из-за известного audio chain блокирует video бага (см. memory feedback).

Цель Phase 7 — реализовать audio mixing в composer'е/около него чтобы:

  1. Получить стабильное video + audio в одном потоке
  2. Retire pipeline'а полностью

Статус 2026-06-03: не достигнута, откатано на parallel mode.


Что в итоге работает (текущее prod-состояние)

rtsp://...:554/live     — pipeline, H264+AAC, подёргивается уже месяц (audio mix bag)
rtsp://...:554/cfc-grid — composer, H264 only, стабильное video
rtsp://...:554/live-audio — cuda-grid-audio, AAC only

TV/VLC рекомендуется использовать cfc-grid для video и (опционально) синхронизировать live-audio параллельно на стороне reader'а до того как Phase 7 будет завершён.


Хронология попыток и почему каждая не сработала

Попытка 1: cfc-grid-ffmpeg mux — raw H.264 в pipe + RTSP audio → -c copy

ffmpeg -f h264 -i /pipe/grid.h264 -i rtsp://.../live-audio -c copy -map 0:v -map 1:a -f rtsp ...

Проблема: Raw H.264 в pipe не имеет PTS/DTS на container-уровне. ffmpeg синтезирует через -r 25 или wallclock, audio RTSP с правильными RTP timestamps, interleave broken. Mediamtx drops video либо audio. Symptoms: 4 fps video (от 25), Could not write header, audio с перебоями.

Что я пробовал из костылей (всё бесплодно):

  • -use_wallclock_as_timestamps 1 на video — ломает audio DTS (Non-monotonic DTS)
  • -fflags +genpts+igndts+discardcorrupt — не помогает
  • -thread_queue_size 32/1024 — без эффекта
  • -muxdelay 0 -max_delay 0 — без эффекта
  • -flush_packets 1 — без эффекта

Попытка 2: decode→re-encode через NVDEC+NVENC в cfc-grid-ffmpeg

-hwaccel cuda -i /pipe/grid.h264 -c:v h264_nvenc -intra_refresh 1 -no-scenecut 1 ...

Проблема: Broken pipe на RTSP output mediamtx. Также double-encode на GPU удваивает NVENC sessions (на Pascal GTX 1050 это уже впритык, на 5090 OK но всё равно лишний overhead).

Попытка 3: композитор пишет mpegts container нативно (Phase 7 core)

Архитектура: composer через libavformat → mpegts container → Unix socket в mediamtx. Решает PTS/DTS на container-уровне + bypass'ит весь split-process pipe→ffmpeg-mux pipeline.

Реализация в src/writer.c, src/audio.c, mediamtx MTX_PATHS_CFCGRID_SOURCE=unix+mpegts:///....

3.1: PPS error non-existing PPS 0 referenced

Причина: libavformat mpegts muxer автоматически инсёртит h264_mp4toannexb bitstream-фильтр, который ожидает AVCC формат (length-prefix NAL units). NVENC выдаёт Annex-B (start codes). При попытке "конвертации" парсер ломается.

Дополнительная инъекция SPS/PPS через NV_ENC_PIC_FLAG_OUTPUT_SPSPPS per-frame (который я добавил пытаясь решить late-joiner проблему) усугубляет — двойная инжекция создаёт мусор для парсера.

Fix: убрать NV_ENC_PIC_FLAG_OUTPUT_SPSPPS per-frame. Оставить только repeatSPSPPS=1 в init. NVENC сам ставит SPS/PPS перед каждым началом intra refresh cycle.

См. также OBS Studio issue #7338 — intra refresh + NVENC + mpegts = известно сломанная пара в индустрии.

3.2: 12 секунд latency

Причины (сложение):

  • -analyzeduration 30000000 -probesize 50000000 в ffmpeg-mux input — 30 секунд hard cap на пробинг (libavformat ждёт байт пока поймёт codec). Для intra refresh stream без IDR ffmpeg вынужден ждать первого refresh cycle.
  • av_interleaved_write_frame буферизует пакеты до max_interleave_delta (default 10 секунд).
  • Default AVIO write buffer 256KB — на 6 Mbps это ~350ms.
  • Named pipe (FIFO) + stdio buffers по 64KB ещё ~100ms.
  • Сложение ≈ 12+ секунд.

Fix частично: AVFMT_FLAG_FLUSH_PACKETS, max_interleave_delta=200000 (200ms), уменьшение thread_queue, -flush_packets 1. Не убирает все источники — глубже композитор/AVIO buffering остаётся.

3.3: omit_video_pes_length не существует во flags

Fix: убрать. Достаточно +resend_headers+pat_pmt_at_frames.

3.4: mediamtx env-var MTX_PATHS_CFC_GRID_SOURCE не разбирается

mediamtx env parser использует _ как разделитель между <NAME> и <FIELD>. Дефис в path name (cfc-grid) ломает pattern → mediamtx думает что path называется cfc а grid_source это что-то другое.

Fix: переименовать path в cfcgrid (без дефиса) либо использовать YAML config file.

3.5: mediamtx закрывает Unix socket — read unix: i/o timeout

Default mediamtx ReadTimeout = 10 секунд. Composer пишет с burst'ами (8 audio packets после каждого video frame), AVIO buffers накапливаются, socket I/O slow → mediamtx видит idle > 10s → disconnect → composer reconnects → cycle.

Partial fix: MTX_READTIMEOUT=30s + MTX_WRITETIMEOUT=30s в mediamtx env. Помогает не полностью.

3.6: malloc_consolidate(): unaligned fastbin chunk detected (heap corruption)

Причина 1 (исправлена): av_packet_alloc() + manual av_malloc(pkt->data) + затем av_interleaved_write_frame — конфликт ownership. av_interleaved_write_frame берёт ownership packet'а через AVBufferRef, но у меня data была вне buffer ref → libav освобождает невалидно.

Fix: использовать av_new_packet(pkt, size) — создаёт proper AVBufferRef, дальше memcpy в pkt->data.

Причина 2 (исправлена): SPSC ring buffer в audio.c — когда полный, producer (audio thread) free'ил oldest данные и двигал tail index. Но tail — это домен consumer'а (main thread). Concurrent modification → use-after-free → heap corruption.

Fix: при full ring drop newest (free(new_pkt.data); return -1), не трогать tail.

3.7: Non-monotonic DTS для audio packets

Audio packets часто приходят пачкой через TCP (несколько AAC frames в одном recv). Если назначать PTS как wallclock_now - start, все они получат идентичный timestamp → non monotonic DTS от mpegts muxer.

Fix: monotonic PTS на основе накопленных samples'ов. AAC-LC = 1024 samples/frame. pts_ns = total_samples * 1_000_000_000 / sample_rate. total_samples += 1024 per frame.

3.8: Video PTS skew с audio PTS

Video PTS было через wall-clock relative to start. Audio PTS — через accumulated samples. Разные clock sources → постепенный drift со временем.

Fix: video PTS тоже через monotonic counter: pts = frame_idx * duration_per_frame, где duration_per_frame = 1/fps в time_base. Это даёт точный 25 fps tempo без skew от composer thread jitter.


Что осталось нерешённым (для будущей попытки)

Даже после всех вышеперечисленных фиксов финальный результат был:

  • Звук с затыками
  • Видео тормозит
  • Периодические разрывы соединения VLC

Гипотезы про оставшийся источник проблем:

  1. mediamtx + Unix socket backpressure под burst write'ами. Когда composer пишет video frame + drain 8 audio packets подряд за один tick, mediamtx видит burst, reader queue заполняется, reader падает. См. MTX_WRITEQUEUESIZE=256 (default слишком мал для burst'ов, увеличение влияет на latency).

  2. AVIO буфер vs AVFMT_FLAG_FLUSH_PACKETS. Возможно flag flush'ит после каждого packet'а, но av_interleaved_write_frame ставит packets в interleave queue ДО flush'а. Net эффект: queue до 200ms (max_interleave_delta), потом burst flush.

  3. mpegts muxer не низколатентный по дизайну. Mediamtx внутренне распаковывает PES → собирает свои RTSP track'и → пакетизирует в RTP. Каждый шаг добавляет buffering. Возможно правильный путь — gortsplib встроенный RTSP publisher (паттерн C из RESEARCH-audio-mixing-low-latency.md), который не использует mpegts контейнер вовсе.

  4. Composer audio drain timing. Я drain'ю до 8 packets после каждого video frame. При 43 audio packets/sec (44.1k/1024) и 25 video frames/sec получается 43/25 = 1.72 packets/frame в среднем. С drain'ом до 8 — большинство фреймов drain'ит 1-2 packets, но при катэппинге очереди (после coмposer pause или audio source jitter) batch'ит до 8. Это может вызывать interleave irregular.

    Альтернатива — drain до 1 packet'а за раз, но N раз пер video tick. Либо drain по wall-clock cadence audio (через timerfd на 23.22ms tick).

  5. Pre-allocation/post-allocation race. Mediamtx ожидает PAT/PMT для path ready'нения. Мой first frame отправляет PAT/PMT (через pat_pmt_at_frames flag), но возможно несколько initial frames проходят без PAT/PMT и mediamtx считает их корруптом.


Что писать ДО следующей попытки

Benchmark-driven research — недостаточно теоретического плана. Нужно:

  1. Замерить реальную latency на каждой stage'и через timestamps в логах:

    • composer compose_compose finishencode_frame begin
    • encode_frame beginon_bitstream callback
    • on_bitstreamav_interleaved_write_frame return
    • av_interleaved_write_frame → byte appears в /run/mediamtx/sock (через strace -e write)
    • mediamtx receives → mediamtx sends RTP packet (через mediamtx logs DEBUG)
    • RTP packet → VLC decoder ready (VLC verbose logs)
  2. Сравнить с baseline'ом — cuda-grid-pipeline даёт <1s end-to-end пока audio mix не block'ает video. Что у него отличается архитектурно? Один процесс, один muxer, один encoder thread. Mediamtx видит rtsp publisher вместо unix-mpegts publisher.

  3. **Изучить mediamtx mpeg-ts internalsunix+mpegts:// source насколько хорошо протестирован? Возможно есть known bag'и. Issue tracker bluenviron/mediamtx.

  4. Альтернативные пути:

    • rtsp publisher через gortsplibgortsplib это Go библиотека, не C, но можно попробовать обвязку через CGo или standalone Go-процесс с TCP control от composer'а.
    • RTP-based publisher напрямую — собрать H.264 NAL units в RTP packets (RFC 6184), AAC в mpeg4-generic RTP (RFC 3640), отправлять на mediamtx RTSP ANNOUNCE/RECORD session. Самый low-latency но самый много кода (~1500 строк).
  5. Подумать про прочный workaround: 2 отдельных RTSP path'а (cfc-grid для video, live-audio для audio) и client-side sync. VLC/mpv поддерживают это через --audio-file=rtsp://...:554/live-audio. Это не "правильный" монолитный поток но зато стабильное.


Список созданных файлов (остались в репо для будущей доработки)

include/cuframes_composer/writer.h    — public API: cfc_writer_create/write/write_audio/close
                                          Поддерживает h264 raw + mpegts container
                                          formats. Audio stream опциональный.
include/cuframes_composer/audio.h     — public API: cfc_audio_create/get_codec_params/
                                          drain/destroy. SPSC ring buffer architecture.

src/writer.c                          — backend impl. Lessons: av_new_packet vs av_malloc,
                                          AVFMT_FLAG_FLUSH_PACKETS, max_interleave_delta=200000,
                                          monotonic video PTS на frame_idx,
                                          monotonic audio PTS на total_samples
src/audio.c                           — audio thread, lock-free SPSC ring (drop newest при
                                          full), RTSP open с low-latency options
                                          (nobuffer+flush_packets, probesize=32768,
                                          analyzeduration=0), AAC parameters discovery

src/nvenc.c                           — encoder. Lesson: убрать NV_ENC_PIC_FLAG_OUTPUT_SPSPPS
                                          per-frame, оставить только repeatSPSPPS=1

examples/grid_record.c                — --audio-source=rtsp://... + --format=mpegts options
docs/RESEARCH-audio-mixing-low-latency.md — research-doc с industry patterns и
                                          recommended approach (паттерн B = mpegts через
                                          Unix socket)
docs/LESSONS-audio-mixing-attempts.md — этот файл

gx/cuframes-composer:0.6 image содержит всё необходимое (libavformat-dev в builder, libavformat58 в runtime). Audio mixing включается через --format=mpegts --audio-source=rtsp://...:8554/live-audio в command'е.

Для возвращения к Phase 7 деплою — раскомментировать MTX_PATHS_CFCGRID_SOURCE в mediamtx env vars + изменить cfc-grid command в compose. См. git history localhost-infra для exact diff'ов.


TL;DR для следующего раза

  1. Не повторять добавление NV_ENC_PIC_FLAG_OUTPUT_SPSPPS per-frame — двойная инжекция SPS/PPS ломает downstream parsers.
  2. Не повторять SPSC ring где producer трогает tail (consumer's side) — heap corruption. Drop newest, не oldest.
  3. Не использовать av_packet_alloc + manual av_malloc(data) с av_interleaved_write_frame — нужен av_new_packet для AVBufferRef.
  4. Не использовать wall-clock PTS если есть монотонный счётчик (frame_idx / total_samples) — гарантирует sync video/audio.
  5. Audio packets приходят пачками через TCP — нельзя назначать PTS = wallclock(now).
  6. mpegts muxer + h264_mp4toannexb BSF — несовместим с intra refresh без careful setup. См. OBS issue #7338.
  7. mediamtx env-var path names не могут иметь - (parser ломается на _ boundary).
  8. Default mediamtx ReadTimeout=10s — слишком короткий для composer'а с burst write'ами.
  9. analyzeduration/probesize в ffmpeg input — главные виновники multi-second startup latency, не реальной streaming latency.
  10. Не пытаться "наobum experiments" с flag'ами — каждое изменение должно быть основано на measurement или цитате из доков. Иначе rabbit hole'у нет конца.

Главный insight: даже идеально сделанный mpegts через Unix socket путь имеет inherent buffering, который не убрать без переходом на native RTP/RTSP publisher. Возможно, правильный финальный путь — gortsplib (паттерн C из research-doc), но это +1500 строк кода.