Files
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

283 lines
9.7 KiB
C
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.
/* simple_record — Phase 1 smoke test.
*
* Подписывается на один cuframes-источник, кодирует каждый кадр через
* NVENC и пишет H.264 Annex-B byte stream в файл. Завершение по SIGINT.
*
* Цель — доказать end-to-end zero-copy: NV12 frame из VMM-памяти publisher'а
* напрямую попадает в NVENC без промежуточных копий через CPU.
*
* Использование:
* simple_record --key cam-parking --out test.h264 [--fps 25] [--bitrate 5000]
*
* Полученный файл проверяется через ffmpeg:
* ffprobe test.h264 → должно показать h264 stream
* ffmpeg -i test.h264 ... → должен декодироваться
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/nvenc.h"
#include "../include/cuframes_composer/source.h"
#include "../include/cuframes_composer/writer.h"
#include <cuda.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static volatile sig_atomic_t g_stop = 0;
static void on_sigint(int sig)
{
(void)sig;
g_stop = 1;
}
/* user-data, передаваемая encoder callback'у */
typedef struct write_ctx {
cfc_writer_t *writer;
uint64_t bytes_written;
uint64_t frames_encoded;
uint64_t idr_count;
} write_ctx_t;
static void on_bitstream(const uint8_t *bs, size_t size, int64_t pts_ns,
int is_idr, void *user)
{
write_ctx_t *ctx = (write_ctx_t *)user;
if (cfc_writer_write(ctx->writer, bs, size, pts_ns, is_idr) == 0) {
ctx->bytes_written += size;
ctx->frames_encoded++;
if (is_idr) ctx->idr_count++;
}
}
static const char *cu_err(CUresult r)
{
const char *s = NULL;
cuGetErrorString(r, &s);
return s ? s : "unknown";
}
#define CUCHECK(expr) do { \
CUresult _r = (expr); \
if (_r != CUDA_SUCCESS) { \
fprintf(stderr, "[simple_record] %s failed: %s\n", #expr, cu_err(_r)); \
return 1; \
} \
} while (0)
int main(int argc, char **argv)
{
const char *key = NULL;
const char *out_path = NULL;
int fps = 25;
int bitrate_kbps = 5000;
int max_seconds = 0; /* 0 = до SIGINT */
const char *out_format = "h264";
static struct option opts[] = {
{"key", required_argument, 0, 'k'},
{"out", required_argument, 0, 'o'},
{"fps", required_argument, 0, 'f'},
{"bitrate", required_argument, 0, 'b'},
{"seconds", required_argument, 0, 's'},
{"format", required_argument, 0, 'F'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "k:o:f:b:s:F:h", opts, NULL)) != -1) {
switch (c) {
case 'k': key = optarg; break;
case 'o': out_path = optarg; break;
case 'f': fps = atoi(optarg); break;
case 'b': bitrate_kbps = atoi(optarg); break;
case 's': max_seconds = atoi(optarg); break;
case 'F': out_format = optarg; break;
case 'h':
default:
fprintf(stderr,
"Использование: %s --key <cuframes-key> --out <file.h264>\n"
" [--fps 25] [--bitrate 5000] [--seconds N]\n",
argv[0]);
return c == 'h' ? 0 : 1;
}
}
if (!key || !out_path) {
fprintf(stderr, "[simple_record] требуются --key и --out\n");
return 1;
}
signal(SIGINT, on_sigint);
signal(SIGTERM, on_sigint);
/* 1) CUDA primary context на устройстве 0. cuframes-subscriber и NVENC
* оба должны работать в одном контексте. */
CUCHECK(cuInit(0));
CUdevice dev;
CUCHECK(cuDeviceGet(&dev, 0));
CUcontext ctx;
CUCHECK(cuDevicePrimaryCtxRetain(&ctx, dev));
CUCHECK(cuCtxPushCurrent(ctx));
/* 2) Открыть source. */
cfc_source_config_t scfg = {
.key = key,
.consumer_name = "simple_record",
.cuda_device = 0,
};
cfc_source_t *src = NULL;
if (cfc_source_create(&scfg, &src) != 0) {
fprintf(stderr, "[simple_record] cfc_source_create failed\n");
return 1;
}
/* 3) Ждать первый кадр чтобы узнать размер. До 30 секунд. */
cfc_source_snapshot_t snap = { 0 };
int waited_ms = 0;
while (!g_stop && waited_ms < 30000) {
cfc_source_get_latest(src, &snap);
if (snap.state == CFC_SOURCE_ACTIVE && snap.width > 0) break;
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50 * 1000 * 1000};
nanosleep(&ts, NULL);
waited_ms += 50;
}
if (g_stop) goto cleanup_src;
if (snap.width <= 0 || snap.height <= 0) {
fprintf(stderr,
"[simple_record] не дождался первого кадра за 30с (state=%d)\n",
snap.state);
goto cleanup_src;
}
fprintf(stderr,
"[simple_record] первый кадр: %dx%d pitch=%d → создаю encoder\n",
snap.width, snap.height, snap.pitch_y);
/* 4) Создать encoder под полученный размер. */
cfc_encoder_config_t ecfg = {
.cuda_ctx = ctx,
.width = snap.width,
.height = snap.height,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate_kbps,
.gop_size = fps, /* IDR раз в секунду — стандарт для RTSP */
.num_b_frames = 0, /* low-latency: B-кадры мешают */
.preset = "ll",
};
cfc_encoder_t *enc = NULL;
if (cfc_encoder_create(&ecfg, &enc) != 0) {
fprintf(stderr, "[simple_record] cfc_encoder_create failed\n");
goto cleanup_src;
}
/* 5) Открыть writer (h264 raw или mpegts container). */
uint8_t spspps[256]; size_t spspps_len = sizeof(spspps);
cfc_encoder_get_sequence_params(enc, spspps, &spspps_len);
cfc_writer_config_t wcfg = {
.path = out_path,
.format = out_format,
.width = snap.width,
.height = snap.height,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate_kbps,
.extradata = spspps,
.extradata_size = spspps_len,
};
write_ctx_t wctx = { 0 };
if (cfc_writer_create(&wcfg, &wctx.writer) != 0) {
fprintf(stderr, "[simple_record] cfc_writer_create failed\n");
goto cleanup_enc;
}
/* 6) Главный цикл — забираем кадры по seq, кодируем. */
uint64_t last_seq = 0;
int64_t start_us;
struct timespec ts_start;
clock_gettime(CLOCK_MONOTONIC, &ts_start);
start_us = (int64_t)ts_start.tv_sec * 1000000 + ts_start.tv_nsec / 1000;
fprintf(stderr, "[simple_record] начало записи в %s (Ctrl+C для остановки)\n",
out_path);
while (!g_stop) {
cfc_source_get_latest(src, &snap);
if (snap.state != CFC_SOURCE_ACTIVE) {
/* Источник не active — короткий sleep и снова. */
struct timespec ts = {.tv_sec = 0, .tv_nsec = 10 * 1000 * 1000};
nanosleep(&ts, NULL);
continue;
}
if (snap.seq == last_seq) {
/* Тот же кадр что и был — ждём новый. Спим четверть кадрового
* интервала чтобы не крутить CPU впустую. */
int sleep_ns = 1000000000 / fps / 4;
struct timespec ts = {.tv_sec = 0, .tv_nsec = sleep_ns};
nanosleep(&ts, NULL);
continue;
}
last_seq = snap.seq;
if (cfc_encoder_encode_frame(enc, snap.ptr, snap.pitch_y,
snap.pts_ns, on_bitstream, &wctx) != 0) {
fprintf(stderr, "[simple_record] encode_frame failed\n");
break;
}
/* Прогресс каждые 100 кадров. */
if (wctx.frames_encoded > 0 && wctx.frames_encoded % 100 == 0) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t now_us = (int64_t)now.tv_sec * 1000000 + now.tv_nsec / 1000;
double elapsed_s = (now_us - start_us) / 1e6;
fprintf(stderr,
"[simple_record] %llu кадров, %llu IDR, %.1f МБ за %.1fс (%.1f fps)\n",
(unsigned long long)wctx.frames_encoded,
(unsigned long long)wctx.idr_count,
wctx.bytes_written / 1048576.0,
elapsed_s,
wctx.frames_encoded / elapsed_s);
}
if (max_seconds > 0) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t elapsed = ((int64_t)now.tv_sec * 1000000 + now.tv_nsec / 1000) - start_us;
if (elapsed / 1000000 >= max_seconds) {
fprintf(stderr, "[simple_record] достигнут лимит %dс\n", max_seconds);
break;
}
}
}
fprintf(stderr, "[simple_record] flush encoder\n");
cfc_encoder_flush(enc, on_bitstream, &wctx);
fprintf(stderr,
"[simple_record] итого: %llu кадров, %llu IDR, %.2f МБ\n",
(unsigned long long)wctx.frames_encoded,
(unsigned long long)wctx.idr_count,
wctx.bytes_written / 1048576.0);
cfc_writer_close(wctx.writer);
cleanup_enc:
cfc_encoder_destroy(enc);
cleanup_src:
cfc_source_destroy(src);
cuCtxPopCurrent(NULL);
cuDevicePrimaryCtxRelease(dev);
return 0;
}