Несколько сессий попыток реализовать 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.
18 KiB
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'е/около него чтобы:
- Получить стабильное video + audio в одном потоке
- 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
Гипотезы про оставшийся источник проблем:
-
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). -
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. -
mpegtsmuxer не низколатентный по дизайну. Mediamtx внутренне распаковывает PES → собирает свои RTSP track'и → пакетизирует в RTP. Каждый шаг добавляет buffering. Возможно правильный путь —gortsplibвстроенный RTSP publisher (паттерн C из RESEARCH-audio-mixing-low-latency.md), который не использует mpegts контейнер вовсе. -
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).
-
Pre-allocation/post-allocation race. Mediamtx ожидает PAT/PMT для path ready'нения. Мой first frame отправляет PAT/PMT (через
pat_pmt_at_framesflag), но возможно несколько initial frames проходят без PAT/PMT и mediamtx считает их корруптом.
Что писать ДО следующей попытки
Benchmark-driven research — недостаточно теоретического плана. Нужно:
-
Замерить реальную latency на каждой stage'и через timestamps в логах:
composer compose_compose finish→encode_frame beginencode_frame begin→on_bitstream callbackon_bitstream→av_interleaved_write_frame returnav_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)
-
Сравнить с baseline'ом — cuda-grid-pipeline даёт
<1s end-to-endпока audio mix не block'ает video. Что у него отличается архитектурно? Один процесс, один muxer, один encoder thread. Mediamtx видит rtsp publisher вместо unix-mpegts publisher. -
**Изучить mediamtx mpeg-ts internals —
unix+mpegts://source насколько хорошо протестирован? Возможно есть known bag'и. Issue tracker bluenviron/mediamtx. -
Альтернативные пути:
- rtsp publisher через gortsplib —
gortsplibэто 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 строк).
- rtsp publisher через gortsplib —
-
Подумать про прочный 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 для следующего раза
- Не повторять добавление
NV_ENC_PIC_FLAG_OUTPUT_SPSPPSper-frame — двойная инжекция SPS/PPS ломает downstream parsers. - Не повторять SPSC ring где producer трогает
tail(consumer's side) — heap corruption. Drop newest, не oldest. - Не использовать
av_packet_alloc+ manualav_malloc(data)сav_interleaved_write_frame— нуженav_new_packetдля AVBufferRef. - Не использовать wall-clock PTS если есть монотонный счётчик (frame_idx / total_samples) — гарантирует sync video/audio.
- Audio packets приходят пачками через TCP — нельзя назначать PTS = wallclock(now).
- mpegts muxer + h264_mp4toannexb BSF — несовместим с intra refresh без careful setup. См. OBS issue #7338.
- mediamtx env-var path names не могут иметь
-(parser ломается на_boundary). - Default mediamtx ReadTimeout=10s — слишком короткий для composer'а с burst write'ами.
- analyzeduration/probesize в ffmpeg input — главные виновники multi-second startup latency, не реальной streaming latency.
- Не пытаться "наobum experiments" с flag'ами — каждое изменение должно быть основано на measurement или цитате из доков. Иначе rabbit hole'у нет конца.
Главный insight: даже идеально сделанный mpegts через Unix socket путь имеет inherent buffering, который не убрать без переходом на native RTP/RTSP publisher. Возможно, правильный финальный путь — gortsplib (паттерн C из research-doc), но это +1500 строк кода.