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

289 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```yaml
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
```yaml
-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 finish``encode_frame begin`
- `encode_frame begin``on_bitstream callback`
- `on_bitstream``av_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 internals**`unix+mpegts://` source насколько хорошо
протестирован? Возможно есть known bag'и. Issue tracker bluenviron/mediamtx.
4. **Альтернативные пути**:
- **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 строк).
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 строк кода.