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.
This commit is contained in:
2026-06-03 14:29:56 +01:00
parent 20b5234c41
commit fa6ab3069a
13 changed files with 1276 additions and 52 deletions
+66
View File
@@ -0,0 +1,66 @@
/* cuframes-composer — audio consumer (Phase 7).
*
* Открывает RTSP-вход с AAC stream'ом (от cuda-grid-audio через mediamtx
* либо прямо с микрофона). В отдельном thread'е читает packet'ы и кладёт
* в lock-free SPSC ring buffer. Video-thread (composer main loop) drain'ит
* ring и пишет audio packet'ы в общий mpegts muxer через
* cfc_writer_write_audio.
*
* Architecture:
* audio_thread: avformat_open_input → av_read_frame loop → push to ring
* main thread (video): cfc_audio_drain → cfc_writer_write_audio loop
*
* Failure mode: если RTSP source падает, audio_thread пытается reconnect
* с exponential backoff. Video не блокируется.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_AUDIO_H
#define CUFRAMES_COMPOSER_AUDIO_H
#include "writer.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct cfc_audio_config {
const char *rtsp_url; /* "rtsp://mediamtx:8554/live-audio" */
int reconnect_min_ms; /* default 1000 */
int reconnect_max_ms; /* default 10000 */
} cfc_audio_config_t;
typedef struct cfc_audio cfc_audio_t;
/* Создать audio consumer + запустить background thread.
* Получает первый packet и сохраняет codec params (sample_rate, channels,
* extradata) — caller потом передаёт их в cfc_writer_config_t. */
int cfc_audio_create(const cfc_audio_config_t *cfg, cfc_audio_t **out);
/* Получить codec params (заполняется после первого успешного read).
* Возвращает 0 если params уже доступны, -1 если ещё нет. Caller может
* polling'ом ждать. */
int cfc_audio_get_codec_params(
cfc_audio_t *a,
int *sample_rate,
int *channels,
const uint8_t **extradata,
size_t *extradata_size
);
/* Drain до N audio packet'ов в writer. Не блокируется. Возвращает число
* записанных packet'ов. */
int cfc_audio_drain(cfc_audio_t *a, cfc_writer_t *writer, int max_packets);
/* Остановить thread + освободить ресурсы. */
int cfc_audio_destroy(cfc_audio_t *a);
#ifdef __cplusplus
}
#endif
#endif /* CUFRAMES_COMPOSER_AUDIO_H */
+7
View File
@@ -62,6 +62,13 @@ typedef struct cfc_composer_config {
int reconnect_max_ms; /* default 30000 */
int stale_threshold_ms; /* default 500 */
int dead_threshold_ms; /* default 5000 */
/* Prefix для consumer_name внутри cuframes_subscriber_create.
* default = "composer" (для обратной совместимости с фазами 1-6).
* Уникальный prefix позволяет нескольким composer'ам одновременно
* subscribe к одним и тем же publisher'ам (cfc-grid + cuda-grid-pipeline
* параллельно, у каждого свой namespace). */
const char *consumer_prefix;
} cfc_composer_config_t;
typedef struct cfc_composer cfc_composer_t;
+92
View File
@@ -0,0 +1,92 @@
/* cuframes-composer — output writer abstraction.
*
* Энкодер NVENC отдаёт сжатый H.264 bitstream (Annex-B byte stream) +
* timestamp в callback'е. Writer берёт эту последовательность и пишет
* либо как raw H.264 в файл/stdout, либо как mpegts container с правильными
* PTS/DTS в header'ах PES packet'ов.
*
* Зачем mpegts:
* Raw H.264 в pipe не содержит timestamps на container-уровне; downstream
* ffmpeg-mux вынужден синтезировать PTS из `-r` или wallclock, что вызывает
* desync с audio и drops при mux'ировании с другим потоком. MPEG-TS даёт
* нативные PTS/DTS на каждом PES packet'е → downstream mux работает
* через `-c copy` без проблем.
*
* Форматы:
* "h264" — raw H.264 Annex-B byte stream (как до Phase 6); fwrite в file
* "mpegts" — MPEG-TS container; libavformat avformat_write_header +
* av_interleaved_write_frame
*
* Path semantics:
* "/path/to/file.h264" — обычный файл
* "-" / "pipe:1" / "/dev/stdout" — stdout (для shell-pipe в downstream)
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_WRITER_H
#define CUFRAMES_COMPOSER_WRITER_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct cfc_writer cfc_writer_t;
typedef struct cfc_writer_config {
const char *path; /* "/path/file.ts", "unix:/run/.../sock", "-" */
const char *format; /* "h264" или "mpegts" */
int width, height; /* для mpegts video codecpar */
int fps_num, fps_den; /* для mpegts time_base hint */
int bitrate_kbps; /* для mpegts mux PCR pacing */
/* SPS/PPS — video codec extradata, кладётся в codecpar до write_header.
* Берётся из cfc_encoder_get_sequence_params. Обязательно для mpegts —
* mpegts muxer кладёт SPS/PPS перед первым IDR из extradata. */
const uint8_t *extradata;
size_t extradata_size;
/* Audio stream — если задан, в mpegts будет второй stream (AAC).
* extradata_audio = AudioSpecificConfig (2 байта обычно). */
int has_audio; /* 0 = video-only, 1 = add audio stream */
int audio_sample_rate; /* например 44100 */
int audio_channels; /* например 2 */
const uint8_t *audio_extradata; /* ASC bytes */
size_t audio_extradata_size;
} cfc_writer_config_t;
int cfc_writer_create(const cfc_writer_config_t *cfg, cfc_writer_t **out);
/* Записать один закодированный video frame. is_keyframe ставит флаг
* AV_PKT_FLAG_KEY. Использует av_write_frame (без интерливинг-очереди). */
int cfc_writer_write(
cfc_writer_t *w,
const uint8_t *bitstream,
size_t size,
int64_t pts_ns,
int is_keyframe
);
/* Записать audio packet (AAC ADTS либо raw — определяется extradata).
* Возвращает 0 при успехе. Thread-safe относительно write video?
* Нет — caller обязан вызывать оба write_* из одного thread'а либо
* локироваться. В нашей архитектуре video-thread drain'ит audio-ring
* и пишет сюда сам. */
int cfc_writer_write_audio(
cfc_writer_t *w,
const uint8_t *aac_data,
size_t size,
int64_t pts_ns
);
/* Закрыть writer. Для mpegts вызывает av_write_trailer. */
int cfc_writer_close(cfc_writer_t *w);
#ifdef __cplusplus
}
#endif
#endif /* CUFRAMES_COMPOSER_WRITER_H */