Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions include/ffmpeg/ffmpeg_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ void close_shared_ffmpeg(struct shared_decode_ctx* dec);
* Runs the decode+barrier loop until dec->exit or g_dvledtx_exit are set. */
void* shared_decode_thread(void* arg);

/* -------------------------------------------------------------------------
* Scaler threading
* ---------------------------------------------------------------------- */

/* Resolve the libswscale slice-thread count used for the decode→transport
* colour conversion: half the online CPUs, clamped to [1, 8].
*
* Always returns a value >= 1. */
int ffmpeg_resolve_sws_threads(void);

/* -------------------------------------------------------------------------
* Per-session source — single-session or raw-YUV path
* ---------------------------------------------------------------------- */
Expand Down
10 changes: 7 additions & 3 deletions include/ffmpeg/ffmpeg_frame_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ static inline const char* ffmpeg_fmt_name(enum AVPixelFormat fmt) {
* ---------------------------------------------------------------------- */

/*
* convert_frame_format() — colour-convert src into dst using sws_scale.
* convert_frame_format() — colour-convert / rescale src into dst.
*
* sws_ctx = SwsContext pre-created for the src→dst format/size mapping.
* src = decoded raw frame (e.g. yuv420p from H.264 decoder).
* src_height = source frame height in luma lines.
* dst = pre-allocated output frame (e.g. yuv422p10le).
*
* Returns the number of output rows written (>= 0), or a negative value on
* error (mirrors sws_scale return convention).
* When dst is reference-counted (allocated via av_frame_get_buffer) and
* src_height covers the whole source frame, the multi-threaded
* sws_scale_frame() path is used; otherwise it falls back to sws_scale().
*
* Returns the number of output rows written (> 0), or a negative value on
* error.
*/
int convert_frame_format(struct SwsContext* sws_ctx,
const AVFrame* src, int src_height,
Expand Down
86 changes: 75 additions & 11 deletions src/ffmpeg/ffmpeg_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
#include <string.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
Expand All @@ -26,6 +28,25 @@
* Helpers
* ========================================================================= */

/*
* ffmpeg_resolve_sws_threads() — pick the libswscale slice-thread count.
*
* Auto-select half of the online CPUs, capped at 8: measured on a 20-core
* host, the conversion scales ~6x up to 8 threads and then plateaus (12
* threads is measurably worse than 8 due to slice-granularity and SMT
* contention). The cap also leaves cores for the H.264 decoder threads and
* the MTL lcores.
*/
int ffmpeg_resolve_sws_threads(void) {
long online = sysconf(_SC_NPROCESSORS_ONLN);
if (online < 1) online = 1;

int threads = (int)(online / 2);
if (threads < 1) threads = 1;
if (threads > 8) threads = 8;
return threads;
}


bool is_raw_yuv(const char* filename) {
const char* ext = strrchr(filename, '.');
Expand Down Expand Up @@ -193,7 +214,7 @@ void* shared_decode_thread(void* arg) {
* Both the shared decode path (open_shared_ffmpeg) and the per-session
* decode path (open_ffmpeg_source) need the same sequence:
* avformat_open_input -> find_stream -> find_decoder -> alloc_context ->
* open2 -> sws_getContext -> alloc frames/packet -> alloc yuv_frame buffer.
* open2 -> sws context -> alloc frames/packet -> alloc yuv_frame buffer.
*
* open_ffmpeg_decoder() extracts this common logic. The caller passes in
* pointers to the target struct's fields.
Expand Down Expand Up @@ -291,27 +312,65 @@ static int open_ffmpeg_decoder(
return -1;
}

*out_sws_ctx = sws_getContext(
(*out_codec_ctx)->width, (*out_codec_ctx)->height, (*out_codec_ctx)->pix_fmt,
target_w, target_h, target_fmt,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
/* Build the scaler explicitly rather than with sws_getContext(): the
* "threads" option must be set between allocation and initialisation, and
* sws_getContext() does both in one call. Slice threading is what keeps the
* conversion off the critical path when the source resolution or pixel
* format differs from the transport one. */
int sws_threads = ffmpeg_resolve_sws_threads();
*out_sws_ctx = sws_alloc_context();
Comment thread
sunilnom marked this conversation as resolved.
if (*out_sws_ctx == NULL) {
LOG_ERROR("%s: sws_getContext failed", log_prefix);
LOG_ERROR("%s: sws_alloc_context failed", log_prefix);
avcodec_free_context(out_codec_ctx);
avformat_close_input(out_fmt_ctx);
return -1;
}
av_opt_set_int(*out_sws_ctx, "srcw", (*out_codec_ctx)->width, 0);
av_opt_set_int(*out_sws_ctx, "srch", (*out_codec_ctx)->height, 0);
av_opt_set_int(*out_sws_ctx, "src_format", (*out_codec_ctx)->pix_fmt, 0);
av_opt_set_int(*out_sws_ctx, "dstw", target_w, 0);
av_opt_set_int(*out_sws_ctx, "dsth", target_h, 0);
av_opt_set_int(*out_sws_ctx, "dst_format", target_fmt, 0);
av_opt_set_int(*out_sws_ctx, "sws_flags", SWS_FAST_BILINEAR, 0);
av_opt_set_int(*out_sws_ctx, "threads", sws_threads, 0);
ret = sws_init_context(*out_sws_ctx, NULL, NULL);
if (ret < 0) {
av_strerror(ret, errbuf, sizeof(errbuf));
LOG_ERROR("%s: sws_init_context failed: %s", log_prefix, errbuf);
sws_freeContext(*out_sws_ctx); *out_sws_ctx = NULL;
avcodec_free_context(out_codec_ctx);
avformat_close_input(out_fmt_ctx);
return -1;
}
/* libswscale clamps the request (e.g. to 1 when built without threading). */
int64_t sws_threads_actual = sws_threads;
av_opt_get_int(*out_sws_ctx, "threads", 0, &sws_threads_actual);

*out_av_frame = av_frame_alloc();
*out_yuv_frame = av_frame_alloc();
*out_av_packet = av_packet_alloc();
if (*out_av_frame == NULL || *out_yuv_frame == NULL || *out_av_packet == NULL) {
LOG_ERROR("%s: frame/packet allocation failed", log_prefix);
av_frame_free(out_av_frame);
av_frame_free(out_yuv_frame);
av_packet_free(out_av_packet);
sws_freeContext(*out_sws_ctx); *out_sws_ctx = NULL;
avcodec_free_context(out_codec_ctx);
avformat_close_input(out_fmt_ctx);
return -1;
}

/* yuv_frame must be reference-counted (av_frame_get_buffer, not
* av_image_alloc): sws_scale_frame() reallocates any destination whose
* buf[0] is NULL, which would leak the original buffer and leave the
* caller writing to storage libswscale no longer targets. */
(*out_yuv_frame)->format = target_fmt;
(*out_yuv_frame)->width = target_w;
(*out_yuv_frame)->height = target_h;
ret = av_image_alloc((*out_yuv_frame)->data, (*out_yuv_frame)->linesize,
target_w, target_h, target_fmt, 32);
ret = av_frame_get_buffer(*out_yuv_frame, 32);
if (ret < 0) {
av_strerror(ret, errbuf, sizeof(errbuf));
LOG_ERROR("%s: av_frame_get_buffer failed: %s", log_prefix, errbuf);
av_frame_free(out_av_frame);
av_frame_free(out_yuv_frame);
av_packet_free(out_av_packet);
Expand All @@ -321,11 +380,12 @@ static int open_ffmpeg_decoder(
return -1;
}

LOG_INFO("%s: opened '%s' Codec=%s %dx%d %s -> %dx%d %s",
LOG_INFO("%s: opened '%s' Codec=%s %dx%d %s -> %dx%d %s (sws_threads=%d)",
log_prefix, filename, codec->name,
(*out_codec_ctx)->width, (*out_codec_ctx)->height,
av_get_pix_fmt_name((*out_codec_ctx)->pix_fmt),
target_w, target_h, ffmpeg_fmt_name(target_fmt));
target_w, target_h, ffmpeg_fmt_name(target_fmt),
(int)sws_threads_actual);
return 0;
}

Expand All @@ -336,7 +396,11 @@ static void close_ffmpeg_decoder(
AVFrame** yuv_frame, AVPacket** av_packet) {
if (*av_frame != NULL) av_frame_free(av_frame);
if (*yuv_frame != NULL) {
av_freep(&(*yuv_frame)->data[0]);
/* Reference-counted frames (av_frame_get_buffer) release their storage in
* av_frame_free(). Only a frame whose data[] came from av_image_alloc
* — buf[0] == NULL — needs the explicit free. */
if ((*yuv_frame)->buf[0] == NULL)
av_freep(&(*yuv_frame)->data[0]);
av_frame_free(yuv_frame);
}
if (*av_packet != NULL) av_packet_free(av_packet);
Expand Down
27 changes: 26 additions & 1 deletion src/ffmpeg/ffmpeg_frame_handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* Provides pixel-buffer operations shared by the FFmpeg TX path and the MTL
* TX path:
*
* convert_frame_format() — wraps sws_scale for src→dst colour conversion.
* convert_frame_format() — converts src→dst via sws_scale_frame when the
* destination is reference-counted and the whole
* source frame is available, otherwise falls back
* to single-threaded sws_scale.
* crop_yuv_frame() — copies a rectangular strip from a full-width
* AVFrame into a smaller crop-sized AVFrame.
*
Expand All @@ -35,6 +38,28 @@ int convert_frame_format(struct SwsContext* sws_ctx,
return -1;
}

/* Threaded path.
*
* sws_scale() can never run multi-threaded: it unconditionally redirects to
* the context's first single-threaded slice sub-context
* (libswscale/swscale.c: `if (c->nb_slice_ctx) c = c->slice_ctx[0];`).
* Slice threading is only reachable through the frame API, which requires a
* reference-counted destination and the complete source frame.
*
* sws_scale_frame() returns 0 (not a row count) when the threaded path is
* taken, so a successful conversion is reported as dst->height to preserve
* this function's "rows written" contract. */
if (dst->buf[0] != NULL && src_height == src->height) {
int ret = sws_scale_frame(sws_ctx, dst, src);
if (ret < 0) {
LOG_ERROR("convert_frame_format: sws_scale_frame failed (ret=%d)", ret);
return ret;
}
return dst->height;
}

/* Fallback: partial source slice, or a destination without an AVBuffer
* (e.g. data[] filled by av_image_fill_arrays). Always single-threaded. */
int rows = sws_scale(sws_ctx,
(const uint8_t* const*)src->data,
src->linesize,
Expand Down
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
#include "core/session_manager.h"
#include "util/logger.h"

#define DVLEDTX_VERSION "0.1.0"
#define DVLEDTX_VERSION "1.0.0"
Comment thread
sunilnom marked this conversation as resolved.

/* File-level application context pointer set before signals are installed. */
static struct dvledtx_context* g_app_ptr = NULL;
Expand Down
6 changes: 4 additions & 2 deletions src/util/config_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ static int extract_json_bool(const char* start, const char* end, const char* key
if (pos >= end || *pos != ':') continue;
pos++;
while (pos < end && (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r')) pos++;
if ((size_t)(end - pos) >= 4 && strncmp(pos, "true", 4) == 0) return 1;
if ((size_t)(end - pos) >= 5 && strncmp(pos, "false", 5) == 0) return 0;
if (pos >= end) return -1;
size_t remaining = (size_t)(end - pos);
if (remaining >= 4 && strncmp(pos, "true", 4) == 0) return 1;
if (remaining >= 5 && strncmp(pos, "false", 5) == 0) return 0;
return -1;
}
return -1;
Expand Down
76 changes: 74 additions & 2 deletions tests/test_ffmpeg_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
* is_raw_yuv() — pure extension check
* load_video_source() — branch logic (empty url, raw YUV, missing files, video)
* close_ffmpeg_source() — null-guard + use_ffmpeg==false path
* close_shared_ffmpeg() — null-guard safety
* close_shared_ffmpeg() — null-guard safety, av_image_alloc and
* reference-counted yuv_frame cleanup
* ffmpeg_resolve_sws_threads() — auto-selected range and determinism
*
* send_video_frame(), open_ffmpeg_output(), close_ffmpeg_output(), and
* ffmpeg_decode_and_send() were removed (legacy dead code).
Expand All @@ -21,6 +23,7 @@

#include <stdatomic.h>
#include <stdbool.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -29,6 +32,7 @@
/* Pull in FFmpeg types before dvledtx_context.h which uses AVPixelFormat */
#include <libavutil/pixfmt.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

Expand Down Expand Up @@ -342,9 +346,74 @@ static void test_close_shared_ffmpeg_with_allocated_resources(void **state)
}

/* ==========================================================================
* close_ffmpeg_source — use_ffmpeg=true with allocated resources
* ffmpeg_resolve_sws_threads
* ========================================================================== */

static void test_resolve_sws_threads_auto_within_bounds(void **state)
{
(void)state;
/* The result must be usable as a libswscale thread count regardless of
* the host CPU count. */
int t = ffmpeg_resolve_sws_threads();
assert_true(t >= 1);
assert_true(t <= 8);
}

static void test_resolve_sws_threads_auto_is_deterministic(void **state)
{
(void)state;
assert_int_equal(ffmpeg_resolve_sws_threads(),
ffmpeg_resolve_sws_threads());
}

/* ==========================================================================
* close_shared_ffmpeg — reference-counted yuv_frame (av_frame_get_buffer)
* ========================================================================== */

/* The threaded conversion path requires yuv_frame to own an AVBuffer.
* close_ffmpeg_decoder must then let av_frame_free() release the storage
* instead of calling av_freep(&data[0]) on it, which would double-free. */
static void test_close_shared_ffmpeg_with_refcounted_yuv_frame(void **state)
{
(void)state;
struct shared_decode_ctx dec;
memset(&dec, 0, sizeof(dec));

dec.av_frame = av_frame_alloc();
dec.av_packet = av_packet_alloc();

dec.yuv_frame = av_frame_alloc();
dec.yuv_frame->format = AV_PIX_FMT_YUV444P12LE;
dec.yuv_frame->width = 64;
dec.yuv_frame->height = 32;
assert_int_equal(av_frame_get_buffer(dec.yuv_frame, 32), 0);
assert_non_null(dec.yuv_frame->buf[0]);

/* Built the same way ffmpeg_decoder.c builds it, so the threaded
* sub-contexts are exercised by the free path too. */
dec.sws_ctx = sws_alloc_context();
assert_non_null(dec.sws_ctx);
av_opt_set_int(dec.sws_ctx, "srcw", 64, 0);
av_opt_set_int(dec.sws_ctx, "srch", 32, 0);
av_opt_set_int(dec.sws_ctx, "src_format", AV_PIX_FMT_YUV420P, 0);
av_opt_set_int(dec.sws_ctx, "dstw", 64, 0);
av_opt_set_int(dec.sws_ctx, "dsth", 32, 0);
av_opt_set_int(dec.sws_ctx, "dst_format", AV_PIX_FMT_YUV444P12LE, 0);
av_opt_set_int(dec.sws_ctx, "sws_flags", SWS_FAST_BILINEAR, 0);
av_opt_set_int(dec.sws_ctx, "threads", 4, 0);
assert_true(sws_init_context(dec.sws_ctx, NULL, NULL) >= 0);

close_shared_ffmpeg(&dec);

assert_null(dec.av_frame);
assert_null(dec.yuv_frame);
assert_null(dec.av_packet);
assert_null(dec.sws_ctx);
}

/* ==========================================================================
* close_ffmpeg_source — use_ffmpeg=true with allocated resources
* ========================================================================== */
static void test_close_ffmpeg_source_with_allocated_resources(void **state)
{
(void)state;
Expand Down Expand Up @@ -441,6 +510,9 @@ int main(void)
/* --- close_shared_ffmpeg --- */
cmocka_unit_test(test_close_shared_ffmpeg_all_null_no_crash),
cmocka_unit_test(test_close_shared_ffmpeg_with_allocated_resources),
cmocka_unit_test(test_close_shared_ffmpeg_with_refcounted_yuv_frame),
cmocka_unit_test(test_resolve_sws_threads_auto_within_bounds),
cmocka_unit_test(test_resolve_sws_threads_auto_is_deterministic),

/* --- ffmpeg_decode_next_frame --- */
cmocka_unit_test(test_ffmpeg_decode_next_frame_null_ctx_returns_false),
Expand Down
Loading
Loading