fa6ab3069a
Несколько сессий попыток реализовать 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.
289 lines
18 KiB
Markdown
289 lines
18 KiB
Markdown
# 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 строк кода.
|