Files
cuframes-composer/examples/grid_record.c
T
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

569 lines
23 KiB
C
Raw 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.
/* grid_record — Phase 2 smoke test.
*
* Подписывается на 4 cuframes-источника (4 камеры), композирует их в 2×2 grid
* через cfc_composer, кодирует через NVENC, пишет H.264 в файл.
*
* Layout 2×2 1080p:
* Output: 3840×2160 (4K)
* Cells: 4 шт. 1920×1080 в углах
*
* Использование:
* grid_record --out 4k.h264 \
* --cell cam-parking,0,0,1920,1080 \
* --cell cam-back_yard,1920,0,1920,1080 \
* --cell cam-front_yard,0,1080,1920,1080 \
* --cell cam-gate_lpr,1920,1080,1920,1080 \
* --seconds 15
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/composer.h"
#include "../include/cuframes_composer/nvenc.h"
#include "../include/cuframes_composer/overlay.h"
#include "../include/cuframes_composer/control.h"
#include "../include/cuframes_composer/health.h"
#include "../include/cuframes_composer/writer.h"
#include "../include/cuframes_composer/audio.h"
#include <cuda.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define MAX_CELLS 16
static volatile sig_atomic_t g_stop = 0;
static void on_sig(int s) { (void)s; g_stop = 1; }
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 int parse_cell(const char *arg, cfc_composer_cell_t *out,
char *key_storage)
{
/* Формат: key,x,y,w,h */
char buf[128];
strncpy(buf, arg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ",");
if (!tok) return -1;
strncpy(key_storage, tok, 63);
key_storage[63] = '\0';
out->source_key = key_storage;
tok = strtok(NULL, ","); if (!tok) return -1; out->x = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->y = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->w = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->h = atoi(tok);
return 0;
}
static const char *cu_err(CUresult r)
{
const char *s = NULL;
cuGetErrorString(r, &s);
return s ? s : "?";
}
int main(int argc, char **argv)
{
const char *out_path = NULL;
/* Default'ы: 1080p / 4 Mbps подходят для GTX 1050 (Pascal), на которой
* крутится production. 4K требует --width 3840 --height 2160 явно. */
int fps = 25, bitrate = 4000, max_seconds = 0;
int out_w = 1920, out_h = 1080;
int border_thickness = 0; /* 0 = без border'ов */
int intra_refresh = 0; /* 1 = intra refresh вместо IDR (low-latency multi-source) */
cfc_composer_cell_t cells[MAX_CELLS] = { 0 };
static char cell_keys[MAX_CELLS][64];
int num_cells = 0;
/* --icon path,x,y[,alpha] */
typedef struct { const char *path; int x, y, alpha; } icon_spec_t;
icon_spec_t icons[MAX_CELLS] = { 0 };
int num_icons = 0;
/* --text font,size,r,g,b,x,y,text — формат хранения как есть в спецификации.
* Также можно префиксовать аргумент через "id=NAME:" — тогда overlay получает
* назначенный ID для управления через control plane. */
typedef struct { const char *font, *text; int size, r, g, b, x, y;
char id[32]; } text_spec_t;
text_spec_t texts[MAX_CELLS] = { 0 };
int num_texts = 0;
const char *control_endpoint = NULL; /* --control tcp://0.0.0.0:5599 */
const char *mqtt_host = NULL; /* --mqtt host[:port] */
int mqtt_port = 1883;
const char *mqtt_instance = "cfc-grid"; /* --mqtt-instance NAME */
const char *mqtt_user = NULL;
const char *mqtt_pass = NULL;
const char *out_format = "h264"; /* --format h264|mpegts */
const char *audio_source = NULL; /* --audio-source rtsp://.../live-audio */
static struct option opts[] = {
{"out", required_argument, 0, 'o'},
{"cell", required_argument, 0, 'c'},
{"fps", required_argument, 0, 'f'},
{"bitrate", required_argument, 0, 'b'},
{"width", required_argument, 0, 'W'},
{"height", required_argument, 0, 'H'},
{"seconds", required_argument, 0, 's'},
{"border", required_argument, 0, 'r'}, /* толщина border'ов */
{"icon", required_argument, 0, 'i'}, /* path,x,y[,alpha] */
{"text", required_argument, 0, 't'}, /* font,size,r,g,b,x,y,text */
{"control", required_argument, 0, 'C'}, /* ZMQ bind endpoint */
{"mqtt", required_argument, 0, 'M'}, /* MQTT broker host[:port] */
{"mqtt-instance", required_argument, 0, 'I'}, /* instance ID для топиков */
{"mqtt-user", required_argument, 0, 'U'},
{"mqtt-pass", required_argument, 0, 'P'},
{"intra-refresh", no_argument, 0, 'R'},
{"format", required_argument, 0, 'F'}, /* h264|mpegts */
{"audio-source", required_argument, 0, 'A'}, /* RTSP audio URL */
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:", opts, NULL)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'c':
if (num_cells >= MAX_CELLS) {
fprintf(stderr, "max %d cells\n", MAX_CELLS);
return 1;
}
if (parse_cell(optarg, &cells[num_cells], cell_keys[num_cells]) != 0) {
fprintf(stderr, "invalid --cell '%s' (key,x,y,w,h)\n", optarg);
return 1;
}
num_cells++;
break;
case 'f': fps = atoi(optarg); break;
case 'b': bitrate = atoi(optarg); break;
case 'W': out_w = atoi(optarg); break;
case 'H': out_h = atoi(optarg); break;
case 's': max_seconds = atoi(optarg); break;
case 'r': border_thickness = atoi(optarg); break;
case 'C': control_endpoint = optarg; break;
case 'M': {
mqtt_host = optarg;
const char *colon = strchr(optarg, ':');
if (colon) {
static char host_buf[64];
int n = colon - optarg;
if (n >= (int)sizeof(host_buf)) n = sizeof(host_buf) - 1;
memcpy(host_buf, optarg, n); host_buf[n] = '\0';
mqtt_host = host_buf;
mqtt_port = atoi(colon + 1);
}
break;
}
case 'I': mqtt_instance = optarg; break;
case 'U': mqtt_user = optarg; break;
case 'P': mqtt_pass = optarg; break;
case 'R': intra_refresh = 1; break;
case 'F': out_format = optarg; break;
case 'A': audio_source = optarg; break;
case 't': {
if (num_texts >= MAX_CELLS) { fprintf(stderr, "max %d texts\n", MAX_CELLS); return 1; }
/* Опциональный prefix "id=NAME:" — задаёт control-plane ID. */
const char *spec = optarg;
char id_buf[32] = { 0 };
if (!strncmp(spec, "id=", 3)) {
const char *colon = strchr(spec, ':');
if (colon) {
int n = colon - (spec + 3);
if (n >= (int)sizeof(id_buf)) n = sizeof(id_buf) - 1;
memcpy(id_buf, spec + 3, n);
id_buf[n] = '\0';
spec = colon + 1;
}
}
strncpy(texts[num_texts].id, id_buf, sizeof(texts[num_texts].id) - 1);
/* font,size,r,g,b,x,y,text — text идёт до конца строки (может
* содержать запятые и пробелы). */
static char text_font[MAX_CELLS][256];
static char text_body[MAX_CELLS][256];
char buf[512];
strncpy(buf, spec, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *p = buf;
char *q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; strncpy(text_font[num_texts], p, 255); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].size = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].r = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].g = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].b = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].x = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].y = atoi(p); p = q + 1;
/* Остаток — text body. */
strncpy(text_body[num_texts], p, 255);
texts[num_texts].font = text_font[num_texts];
texts[num_texts].text = text_body[num_texts];
num_texts++;
break;
}
case 'i': {
if (num_icons >= MAX_CELLS) {
fprintf(stderr, "max %d icons\n", MAX_CELLS);
return 1;
}
/* Парсим path,x,y[,alpha] аналогично --cell.
* Спецификация хранится статично — указатель уйдёт в overlay. */
static char icon_paths[MAX_CELLS][256];
char buf[300]; strncpy(buf, optarg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
strncpy(icon_paths[num_icons], tok, 255);
icons[num_icons].path = icon_paths[num_icons];
tok = strtok(NULL, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
icons[num_icons].x = atoi(tok);
tok = strtok(NULL, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
icons[num_icons].y = atoi(tok);
tok = strtok(NULL, ",");
icons[num_icons].alpha = tok ? atoi(tok) : 255;
num_icons++;
break;
}
default: return 1;
}
}
if (!out_path || num_cells == 0) {
fprintf(stderr,
"Использование: %s --out <file.h264> --cell key,x,y,w,h [--cell ...]\n"
" [--width 3840] [--height 2160] [--fps 25]\n"
" [--bitrate 10000] [--seconds N]\n",
argv[0]);
return 1;
}
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
/* CUDA primary context. */
CUresult cr = cuInit(0);
if (cr != CUDA_SUCCESS) { fprintf(stderr, "cuInit: %s\n", cu_err(cr)); return 1; }
CUdevice dev;
cuDeviceGet(&dev, 0);
CUcontext ctx;
cuDevicePrimaryCtxRetain(&ctx, dev);
cuCtxPushCurrent(ctx);
/* Composer. */
cfc_composer_config_t ccfg = {
.width = out_w,
.height = out_h,
.cells = cells,
.num_cells = num_cells,
.cuda_device = 0,
.consumer_prefix = mqtt_instance, /* уникальный namespace на каждый composer */
};
cfc_composer_t *comp = NULL;
if (cfc_composer_create(&ccfg, &comp) != 0) {
fprintf(stderr, "cfc_composer_create failed\n");
return 1;
}
fprintf(stderr, "[grid_record] composer %dx%d, %d ячеек\n",
out_w, out_h, num_cells);
/* TEXT overlays. */
for (int i = 0; i < num_texts; i++) {
cfc_overlay_text_config_t tc = {
.font_path = texts[i].font,
.text = texts[i].text,
.pixel_size = texts[i].size,
.x = texts[i].x, .y = texts[i].y,
.r = texts[i].r, .g = texts[i].g, .b = texts[i].b,
.extra_alpha = 255,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_text(&tc, &ov) != 0) {
fprintf(stderr, "[grid_record] text overlay create failed: '%s'\n",
texts[i].text);
continue;
}
if (texts[i].id[0]) cfc_overlay_set_id(ov, texts[i].id);
int tw = 0, th = 0;
cfc_overlay_text_size(ov, &tw, &th);
cfc_composer_add_overlay(comp, ov);
fprintf(stderr,
"[grid_record] text @ (%d,%d) %dx%d size=%d color=(%d,%d,%d) id='%s' '%s'\n",
texts[i].x, texts[i].y, tw, th, texts[i].size,
texts[i].r, texts[i].g, texts[i].b,
texts[i].id[0] ? texts[i].id : "", texts[i].text);
}
/* MQTT health publisher. */
cfc_health_t *hpub = NULL;
if (mqtt_host) {
cfc_health_config_t hc = {
.host = mqtt_host, .port = mqtt_port,
.username = mqtt_user, .password = mqtt_pass,
.topic_prefix = "composer",
.instance = mqtt_instance,
.interval_sec = 10,
.composer = comp,
.publish_discovery = 1,
};
cfc_health_create(&hc, &hpub);
}
/* Control plane. */
cfc_control_t *ctl = NULL;
if (control_endpoint) {
cfc_control_config_t cc = {
.bind_endpoint = control_endpoint,
.composer = comp,
.cuda_ctx = ctx,
};
if (cfc_control_create(&cc, &ctl) != 0) {
fprintf(stderr, "[grid_record] cfc_control_create failed\n");
}
}
/* PNG иконки. */
for (int i = 0; i < num_icons; i++) {
cfc_overlay_png_config_t pc = {
.path = icons[i].path,
.x = icons[i].x,
.y = icons[i].y,
.extra_alpha = icons[i].alpha,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_png(&pc, &ov) != 0) {
fprintf(stderr, "[grid_record] PNG '%s' load failed\n", icons[i].path);
continue;
}
int iw = 0, ih = 0;
cfc_overlay_png_size(ov, &iw, &ih);
cfc_composer_add_overlay(comp, ov);
fprintf(stderr, "[grid_record] icon '%s' %dx%d @ (%d,%d) alpha=%d\n",
icons[i].path, iw, ih, icons[i].x, icons[i].y, icons[i].alpha);
}
/* Border'ы вокруг каждой ячейки если --border задан.
* Цвет: серо-голубой (BT.709 limited): Y=180, U=120, V=110. */
if (border_thickness > 0) {
for (int i = 0; i < num_cells; i++) {
cfc_overlay_border_config_t bc = {
.x = cells[i].x, .y = cells[i].y,
.w = cells[i].w, .h = cells[i].h,
.thickness = border_thickness,
.color_y = 180, .color_u = 120, .color_v = 110,
.alpha = 220,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_border(&bc, &ov) == 0) {
cfc_composer_add_overlay(comp, ov);
}
}
fprintf(stderr, "[grid_record] добавлены border'ы (толщина %d) для %d ячеек\n",
border_thickness, num_cells);
}
/* Encoder. */
cfc_encoder_config_t ecfg = {
.cuda_ctx = ctx,
.width = out_w,
.height = out_h,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate,
.gop_size = fps,
.num_b_frames = 0,
.preset = "ll",
.intra_refresh = intra_refresh,
.intra_refresh_period = fps, /* полный цикл за 1 секунду */
};
if (intra_refresh) {
fprintf(stderr, "[grid_record] intra refresh ON (period=%d кадров)\n", fps);
}
cfc_encoder_t *enc = NULL;
if (cfc_encoder_create(&ecfg, &enc) != 0) {
fprintf(stderr, "cfc_encoder_create failed\n");
cfc_composer_destroy(comp);
return 1;
}
/* Audio consumer (опциональный, Phase 7). Запускаем РАНЬШЕ writer'а
* чтобы успеть получить codec params (sample_rate, channels, extradata)
* до avformat_write_header — иначе audio stream'у не будет правильного
* setup'а. Polling до 5 секунд. */
cfc_audio_t *audio = NULL;
int audio_sample_rate = 0, audio_channels = 0;
const uint8_t *audio_extradata = NULL;
size_t audio_extradata_size = 0;
if (audio_source) {
cfc_audio_config_t acfg = { .rtsp_url = audio_source };
if (cfc_audio_create(&acfg, &audio) != 0) {
fprintf(stderr, "[grid_record] audio create failed, продолжаю без audio\n");
} else {
fprintf(stderr, "[grid_record] жду audio codec params от %s ...\n", audio_source);
/* 30 секунд polling — audio source (cuda-grid-audio) может ещё
* подниматься после recreate стeка. Audio thread сам retry'ится
* с exp backoff. */
for (int i = 0; i < 300; i++) { /* 300 × 100ms = 30s */
if (cfc_audio_get_codec_params(audio, &audio_sample_rate,
&audio_channels, &audio_extradata,
&audio_extradata_size) == 0) {
fprintf(stderr,
"[grid_record] audio готов: AAC %dHz %dch extradata=%zub\n",
audio_sample_rate, audio_channels, audio_extradata_size);
break;
}
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000 * 1000 };
nanosleep(&ts, NULL);
}
if (audio_sample_rate == 0) {
fprintf(stderr, "[grid_record] audio params не получены за 30с, без audio\n");
cfc_audio_destroy(audio); audio = NULL;
}
}
}
/* Writer: mpegts с video + опциональным audio. */
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 = out_w,
.height = out_h,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate,
.extradata = spspps,
.extradata_size = spspps_len,
.has_audio = audio ? 1 : 0,
.audio_sample_rate = audio_sample_rate,
.audio_channels = audio_channels,
.audio_extradata = audio_extradata,
.audio_extradata_size = audio_extradata_size,
};
write_ctx_t wctx = { 0 };
if (cfc_writer_create(&wcfg, &wctx.writer) != 0) {
fprintf(stderr, "cfc_writer_create(%s, %s) failed\n", out_path, out_format);
if (audio) cfc_audio_destroy(audio);
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
return 1;
}
fprintf(stderr, "[grid_record] начало записи в %s [format=%s%s] (Ctrl+C для остановки)\n",
out_path, out_format, audio ? "+audio" : "");
/* Main loop — frame cadence по wall clock'у. */
struct timespec ts_start;
clock_gettime(CLOCK_MONOTONIC, &ts_start);
int64_t start_us = (int64_t)ts_start.tv_sec * 1000000 + ts_start.tv_nsec / 1000;
int64_t frame_us = 1000000 / fps;
int64_t next_us = start_us;
while (!g_stop) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t now_us = (int64_t)now.tv_sec * 1000000 + now.tv_nsec / 1000;
if (now_us < next_us) {
int64_t sleep_us = next_us - now_us;
if (sleep_us > 1000000) sleep_us = 1000000;
struct timespec ts = {
.tv_sec = sleep_us / 1000000,
.tv_nsec = (sleep_us % 1000000) * 1000,
};
nanosleep(&ts, NULL);
continue;
}
next_us += frame_us;
CUdeviceptr out_y = 0;
int out_pitch = 0, oW = 0, oH = 0;
if (cfc_composer_compose(comp, &out_y, &out_pitch, &oW, &oH) != 0) {
fprintf(stderr, "[grid_record] compose failed\n");
break;
}
int64_t pts_ns = (now_us - start_us) * 1000;
/* Не break'аем при encode/write failure — это обычно временно
* (mediamtx reconnect, socket broken). Просто логируем и продолжаем,
* следующая encode/write попытается заново. */
if (cfc_encoder_encode_frame(enc, out_y, out_pitch, pts_ns,
on_bitstream, &wctx) != 0) {
static int warned = 0;
if (!warned) { fprintf(stderr, "[grid_record] encode failed (продолжаю)\n"); warned = 1; }
}
/* Drain audio packets — пишем сразу после video frame. */
if (audio) cfc_audio_drain(audio, wctx.writer, 8);
if (wctx.frames_encoded > 0 && wctx.frames_encoded % 50 == 0) {
double elapsed = (now_us - start_us) / 1e6;
cfc_composer_health_t h;
cfc_composer_get_health(comp, &h);
fprintf(stderr,
"[grid_record] %llu кадров, %llu IDR, %.1f МБ за %.1fс (%.1f fps) | "
"src active=%d stale=%d dead=%d\n",
(unsigned long long)wctx.frames_encoded,
(unsigned long long)wctx.idr_count,
wctx.bytes_written / 1048576.0,
elapsed,
wctx.frames_encoded / elapsed,
h.active, h.stale, h.dead);
}
if (max_seconds > 0 && (now_us - start_us) / 1000000 >= max_seconds) {
fprintf(stderr, "[grid_record] лимит %dс\n", max_seconds);
break;
}
}
fprintf(stderr, "[grid_record] flush encoder\n");
cfc_encoder_flush(enc, on_bitstream, &wctx);
fprintf(stderr,
"[grid_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);
if (audio) cfc_audio_destroy(audio);
if (ctl) cfc_control_destroy(ctl);
if (hpub) cfc_health_destroy(hpub);
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
cuCtxPopCurrent(NULL);
cuDevicePrimaryCtxRelease(dev);
return 0;
}