Skip to content

Commit 60bfb92

Browse files
sunilnomsunilnom
andauthored
Tx FPS fix by using SWS Threads (#52)
* Add PTP hardware timing support to MTL and FFmpeg TX paths - Wire MTL_FLAG_PTP_ENABLE/PI/UNICAST_ADDR into both the direct MTL TX path (src/mtl/mtl_tx.c) and the FFmpeg mtl_st20p muxer path (src/ffmpeg/ffmpeg_tx.c), with a sync-notify callback logging master_utc_offset/delta. - app_context.h: add ptp_enable/ptp_pi/ptp_unicast fields. - config_reader.c/.h: hardcode PTP defaults (enable=true, pi=true, unicast=false) instead of reading them from JSON. Verified on hardware (Intel I225-V/igc) with both enable_mtl_tx=true and enable_mtl_tx=false builds: PTP enables cleanly (tx pacing ptp, ptp_init pi controller) with no grandmaster present on the test network (sync cnt 0, as expected). * PTP: graceful AVOption handling and review-comment fixes - ffmpeg_tx.c: treat AVERROR_OPTION_NOT_FOUND from PTP AVOptions as a benign case so the app works whether or not the MTL plugin carries the PTP patch (single INFO log instead of repeated WARNs) - config_reader.h: use bool for ptp_enable/ptp_pi/ptp_unicast toggles - app_context.h / mtl_tx.c: clarify PTP comments (used by both the direct MTL TX pipeline and the FFmpeg avdevice path) - config: add tx_gbrp12le_pf_8session.json (8-session pf test config) * Make PTP disabled by default, configurable via JSON "ptp" block PTP-paced TX requires a PTP grandmaster on the network. MTL's built-in PTP client is slave-only and cannot elect itself grandmaster, so with no grandmaster present the TX pacing clock never locks and transmission stalls. Previously ptp_enable/ptp_pi were hardcoded to true in the config reader, forcing this failure mode on every deployment. PTP now defaults to disabled (MTL falls back to TSC-based pacing) and is opted into via an optional top-level "ptp" block: "ptp": { "enable": true, "pi": true, "unicast": false } - Add extract_json_bool() helper to config_reader, mirroring extract_json_int() (returns 1/0 for true/false, -1 when absent). - Parse the optional "ptp" object for enable/pi/unicast; each key falls back to false when omitted. - Log "PTP disabled (default TSC-based TX pacing)" on the default path. - Document the block in the config_reader schema comment and README, including the grandmaster requirement and expected log output. - Point tx_1session_12bit and tx_fullhd_single_session at the local NIC BDF 0000:01:00.1 and correct the latter's source dimensions/format. Verified on an 8-session 1080p yuv422p12le config: PTP disabled selects "pacing way: tsc" and runs clean (0 errors, ~29.8 fps per session, ~1361 Mb/s aggregate); setting "enable": true selects "pacing way: ptp". * ffmpeg: enable libswscale slice threading for format conversion The decode->transport colour conversion ran single-threaded and dominated the per-frame budget whenever the source resolution or pixel format differed from the transport one (26 ms/frame for 1080p gbrp12le -> 4K yuv444p12le against a 33.3 ms budget at 30 fps), capping 4K30 at ~21 fps. sws_scale() can never thread: it redirects to the context's single-threaded first slice sub-context. Build the SwsContext with sws_alloc_context() so the 'threads' option can be set before sws_init_context(), and convert through sws_scale_frame(), which is the only entry point reaching the slice threads. sws_scale_frame() reallocates any destination frame with a NULL buf[0], so yuv_frame moves from av_image_alloc() to av_frame_get_buffer(); the cleanup path now frees the manual buffer only when the frame is not reference-counted. Thread count is auto-selected as min(nproc/2, 8) - the conversion plateaus past 8 threads - and can be overridden via tx_video.sws_threads. Measured 4K30, 1080p gbrp12le source -> yuv444p12le transport: before 20.94 fps 6562.18 Mb/s avg 6592.62 peak after 29.93 fps 9379.15 Mb/s avg 9401.41 peak * scripts: add benchmark harness with --src30/--src60 source pinning benchmark.sh sweeps (resolution, fps, pixel format) combinations, parses the MTL statistics from each run and emits a CSV plus an XLSX report. --src30/--src60 pin the source clip per frame rate so a whole sweep can be run against one input format instead of per-format native sources. * tests: cover swscale slice threading changes convert_frame_format: threaded sws_scale_frame path normalises the 0 return to a row count; a destination without an AVBuffer and a partial source slice must both fall back to sws_scale so the caller's buffer stays the conversion target; repeated threaded conversions must not swap the destination plane. close_ffmpeg_decoder: a reference-counted yuv_frame must be released by av_frame_free alone - the previous unconditional av_freep(&data[0]) double frees it. tx_video.sws_threads: parsing, 0 = auto default, validation bounds and the copy into the app context. resolve_sws_threads is renamed ffmpeg_resolve_sws_threads and declared in the decoder header so the override, the clamp to 64 and the auto range are directly testable. * Remove local benchmark tooling scripts * Remove JSON-configurable sws_threads option Drops tx_video.sws_threads from the config struct, parser, validation and context-apply path. libswscale slice threading still runs via ffmpeg_resolve_sws_threads() auto-detection. * Bump application version to 1.0.0 * Remove obsolete sws_threads unit tests The tx_video.sws_threads config option was removed, so the tests asserting on cfg.sws_threads / app.sws_threads no longer compile. * Drop unreachable sws_threads override, fix frame handler doc The tx_video.sws_threads config option was removed, so app->sws_threads was never populated and ffmpeg_resolve_sws_threads() always took the auto path. Remove the dead field, the configured parameter and the open_ffmpeg_decoder() pass-through so slice threading is auto-only. Also correct the ffmpeg_frame_handler.c header: convert_frame_format() uses sws_scale_frame only for reference-counted destinations with a complete source frame, and otherwise falls back to sws_scale. * Fix cppcheck knownConditionTrueFalse in extract_json_bool cppcheck flagged the '(size_t)(end - pos) >= 4/5' guards as always false because it could not relate the pointer distance to the preceding 'pos < end' loop. Bail out explicitly when pos reaches end and hoist the remaining-byte count into a local, which keeps the same bounds checks without the false positive. --------- Co-authored-by: sunilnom <you@example.com>
1 parent b9992b5 commit 60bfb92

8 files changed

Lines changed: 362 additions & 21 deletions

File tree

include/ffmpeg/ffmpeg_decoder.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ void close_shared_ffmpeg(struct shared_decode_ctx* dec);
3737
* Runs the decode+barrier loop until dec->exit or g_dvledtx_exit are set. */
3838
void* shared_decode_thread(void* arg);
3939

40+
/* -------------------------------------------------------------------------
41+
* Scaler threading
42+
* ---------------------------------------------------------------------- */
43+
44+
/* Resolve the libswscale slice-thread count used for the decode→transport
45+
* colour conversion: half the online CPUs, clamped to [1, 8].
46+
*
47+
* Always returns a value >= 1. */
48+
int ffmpeg_resolve_sws_threads(void);
49+
4050
/* -------------------------------------------------------------------------
4151
* Per-session source — single-session or raw-YUV path
4252
* ---------------------------------------------------------------------- */

include/ffmpeg/ffmpeg_frame_handler.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ static inline const char* ffmpeg_fmt_name(enum AVPixelFormat fmt) {
3131
* ---------------------------------------------------------------------- */
3232

3333
/*
34-
* convert_frame_format() — colour-convert src into dst using sws_scale.
34+
* convert_frame_format() — colour-convert / rescale src into dst.
3535
*
3636
* sws_ctx = SwsContext pre-created for the src→dst format/size mapping.
3737
* src = decoded raw frame (e.g. yuv420p from H.264 decoder).
3838
* src_height = source frame height in luma lines.
3939
* dst = pre-allocated output frame (e.g. yuv422p10le).
4040
*
41-
* Returns the number of output rows written (>= 0), or a negative value on
42-
* error (mirrors sws_scale return convention).
41+
* When dst is reference-counted (allocated via av_frame_get_buffer) and
42+
* src_height covers the whole source frame, the multi-threaded
43+
* sws_scale_frame() path is used; otherwise it falls back to sws_scale().
44+
*
45+
* Returns the number of output rows written (> 0), or a negative value on
46+
* error.
4347
*/
4448
int convert_frame_format(struct SwsContext* sws_ctx,
4549
const AVFrame* src, int src_height,

src/ffmpeg/ffmpeg_decoder.c

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
#include <string.h>
1414
#include <strings.h>
1515
#include <time.h>
16+
#include <unistd.h>
1617
#include <sys/stat.h>
1718

1819
#include <libavformat/avformat.h>
1920
#include <libavcodec/avcodec.h>
2021
#include <libavutil/imgutils.h>
22+
#include <libavutil/opt.h>
2123
#include <libavutil/pixdesc.h>
2224
#include <libswscale/swscale.h>
2325
#include <libavdevice/avdevice.h>
@@ -26,6 +28,25 @@
2628
* Helpers
2729
* ========================================================================= */
2830

31+
/*
32+
* ffmpeg_resolve_sws_threads() — pick the libswscale slice-thread count.
33+
*
34+
* Auto-select half of the online CPUs, capped at 8: measured on a 20-core
35+
* host, the conversion scales ~6x up to 8 threads and then plateaus (12
36+
* threads is measurably worse than 8 due to slice-granularity and SMT
37+
* contention). The cap also leaves cores for the H.264 decoder threads and
38+
* the MTL lcores.
39+
*/
40+
int ffmpeg_resolve_sws_threads(void) {
41+
long online = sysconf(_SC_NPROCESSORS_ONLN);
42+
if (online < 1) online = 1;
43+
44+
int threads = (int)(online / 2);
45+
if (threads < 1) threads = 1;
46+
if (threads > 8) threads = 8;
47+
return threads;
48+
}
49+
2950

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

294-
*out_sws_ctx = sws_getContext(
295-
(*out_codec_ctx)->width, (*out_codec_ctx)->height, (*out_codec_ctx)->pix_fmt,
296-
target_w, target_h, target_fmt,
297-
SWS_FAST_BILINEAR, NULL, NULL, NULL);
315+
/* Build the scaler explicitly rather than with sws_getContext(): the
316+
* "threads" option must be set between allocation and initialisation, and
317+
* sws_getContext() does both in one call. Slice threading is what keeps the
318+
* conversion off the critical path when the source resolution or pixel
319+
* format differs from the transport one. */
320+
int sws_threads = ffmpeg_resolve_sws_threads();
321+
*out_sws_ctx = sws_alloc_context();
298322
if (*out_sws_ctx == NULL) {
299-
LOG_ERROR("%s: sws_getContext failed", log_prefix);
323+
LOG_ERROR("%s: sws_alloc_context failed", log_prefix);
300324
avcodec_free_context(out_codec_ctx);
301325
avformat_close_input(out_fmt_ctx);
302326
return -1;
303327
}
328+
av_opt_set_int(*out_sws_ctx, "srcw", (*out_codec_ctx)->width, 0);
329+
av_opt_set_int(*out_sws_ctx, "srch", (*out_codec_ctx)->height, 0);
330+
av_opt_set_int(*out_sws_ctx, "src_format", (*out_codec_ctx)->pix_fmt, 0);
331+
av_opt_set_int(*out_sws_ctx, "dstw", target_w, 0);
332+
av_opt_set_int(*out_sws_ctx, "dsth", target_h, 0);
333+
av_opt_set_int(*out_sws_ctx, "dst_format", target_fmt, 0);
334+
av_opt_set_int(*out_sws_ctx, "sws_flags", SWS_FAST_BILINEAR, 0);
335+
av_opt_set_int(*out_sws_ctx, "threads", sws_threads, 0);
336+
ret = sws_init_context(*out_sws_ctx, NULL, NULL);
337+
if (ret < 0) {
338+
av_strerror(ret, errbuf, sizeof(errbuf));
339+
LOG_ERROR("%s: sws_init_context failed: %s", log_prefix, errbuf);
340+
sws_freeContext(*out_sws_ctx); *out_sws_ctx = NULL;
341+
avcodec_free_context(out_codec_ctx);
342+
avformat_close_input(out_fmt_ctx);
343+
return -1;
344+
}
345+
/* libswscale clamps the request (e.g. to 1 when built without threading). */
346+
int64_t sws_threads_actual = sws_threads;
347+
av_opt_get_int(*out_sws_ctx, "threads", 0, &sws_threads_actual);
304348

305349
*out_av_frame = av_frame_alloc();
306350
*out_yuv_frame = av_frame_alloc();
307351
*out_av_packet = av_packet_alloc();
352+
if (*out_av_frame == NULL || *out_yuv_frame == NULL || *out_av_packet == NULL) {
353+
LOG_ERROR("%s: frame/packet allocation failed", log_prefix);
354+
av_frame_free(out_av_frame);
355+
av_frame_free(out_yuv_frame);
356+
av_packet_free(out_av_packet);
357+
sws_freeContext(*out_sws_ctx); *out_sws_ctx = NULL;
358+
avcodec_free_context(out_codec_ctx);
359+
avformat_close_input(out_fmt_ctx);
360+
return -1;
361+
}
308362

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

324-
LOG_INFO("%s: opened '%s' Codec=%s %dx%d %s -> %dx%d %s",
383+
LOG_INFO("%s: opened '%s' Codec=%s %dx%d %s -> %dx%d %s (sws_threads=%d)",
325384
log_prefix, filename, codec->name,
326385
(*out_codec_ctx)->width, (*out_codec_ctx)->height,
327386
av_get_pix_fmt_name((*out_codec_ctx)->pix_fmt),
328-
target_w, target_h, ffmpeg_fmt_name(target_fmt));
387+
target_w, target_h, ffmpeg_fmt_name(target_fmt),
388+
(int)sws_threads_actual);
329389
return 0;
330390
}
331391

@@ -336,7 +396,11 @@ static void close_ffmpeg_decoder(
336396
AVFrame** yuv_frame, AVPacket** av_packet) {
337397
if (*av_frame != NULL) av_frame_free(av_frame);
338398
if (*yuv_frame != NULL) {
339-
av_freep(&(*yuv_frame)->data[0]);
399+
/* Reference-counted frames (av_frame_get_buffer) release their storage in
400+
* av_frame_free(). Only a frame whose data[] came from av_image_alloc
401+
* — buf[0] == NULL — needs the explicit free. */
402+
if ((*yuv_frame)->buf[0] == NULL)
403+
av_freep(&(*yuv_frame)->data[0]);
340404
av_frame_free(yuv_frame);
341405
}
342406
if (*av_packet != NULL) av_packet_free(av_packet);

src/ffmpeg/ffmpeg_frame_handler.c

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
* Provides pixel-buffer operations shared by the FFmpeg TX path and the MTL
99
* TX path:
1010
*
11-
* convert_frame_format() — wraps sws_scale for src→dst colour conversion.
11+
* convert_frame_format() — converts src→dst via sws_scale_frame when the
12+
* destination is reference-counted and the whole
13+
* source frame is available, otherwise falls back
14+
* to single-threaded sws_scale.
1215
* crop_yuv_frame() — copies a rectangular strip from a full-width
1316
* AVFrame into a smaller crop-sized AVFrame.
1417
*
@@ -35,6 +38,28 @@ int convert_frame_format(struct SwsContext* sws_ctx,
3538
return -1;
3639
}
3740

41+
/* Threaded path.
42+
*
43+
* sws_scale() can never run multi-threaded: it unconditionally redirects to
44+
* the context's first single-threaded slice sub-context
45+
* (libswscale/swscale.c: `if (c->nb_slice_ctx) c = c->slice_ctx[0];`).
46+
* Slice threading is only reachable through the frame API, which requires a
47+
* reference-counted destination and the complete source frame.
48+
*
49+
* sws_scale_frame() returns 0 (not a row count) when the threaded path is
50+
* taken, so a successful conversion is reported as dst->height to preserve
51+
* this function's "rows written" contract. */
52+
if (dst->buf[0] != NULL && src_height == src->height) {
53+
int ret = sws_scale_frame(sws_ctx, dst, src);
54+
if (ret < 0) {
55+
LOG_ERROR("convert_frame_format: sws_scale_frame failed (ret=%d)", ret);
56+
return ret;
57+
}
58+
return dst->height;
59+
}
60+
61+
/* Fallback: partial source slice, or a destination without an AVBuffer
62+
* (e.g. data[] filled by av_image_fill_arrays). Always single-threaded. */
3863
int rows = sws_scale(sws_ctx,
3964
(const uint8_t* const*)src->data,
4065
src->linesize,

src/main.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
#include "core/session_manager.h"
2525
#include "util/logger.h"
2626

27-
#define DVLEDTX_VERSION "0.1.0"
27+
#define DVLEDTX_VERSION "1.0.0"
2828

2929
/* File-level application context pointer set before signals are installed. */
3030
static struct dvledtx_context* g_app_ptr = NULL;

src/util/config_reader.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ static int extract_json_bool(const char* start, const char* end, const char* key
123123
if (pos >= end || *pos != ':') continue;
124124
pos++;
125125
while (pos < end && (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r')) pos++;
126-
if ((size_t)(end - pos) >= 4 && strncmp(pos, "true", 4) == 0) return 1;
127-
if ((size_t)(end - pos) >= 5 && strncmp(pos, "false", 5) == 0) return 0;
126+
if (pos >= end) return -1;
127+
size_t remaining = (size_t)(end - pos);
128+
if (remaining >= 4 && strncmp(pos, "true", 4) == 0) return 1;
129+
if (remaining >= 5 && strncmp(pos, "false", 5) == 0) return 0;
128130
return -1;
129131
}
130132
return -1;

tests/test_ffmpeg_decoder.c

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
* is_raw_yuv() — pure extension check
99
* load_video_source() — branch logic (empty url, raw YUV, missing files, video)
1010
* close_ffmpeg_source() — null-guard + use_ffmpeg==false path
11-
* close_shared_ffmpeg() — null-guard safety
11+
* close_shared_ffmpeg() — null-guard safety, av_image_alloc and
12+
* reference-counted yuv_frame cleanup
13+
* ffmpeg_resolve_sws_threads() — auto-selected range and determinism
1214
*
1315
* send_video_frame(), open_ffmpeg_output(), close_ffmpeg_output(), and
1416
* ffmpeg_decode_and_send() were removed (legacy dead code).
@@ -21,6 +23,7 @@
2123

2224
#include <stdatomic.h>
2325
#include <stdbool.h>
26+
#include <limits.h>
2427
#include <stdio.h>
2528
#include <stdlib.h>
2629
#include <string.h>
@@ -29,6 +32,7 @@
2932
/* Pull in FFmpeg types before dvledtx_context.h which uses AVPixelFormat */
3033
#include <libavutil/pixfmt.h>
3134
#include <libavutil/imgutils.h>
35+
#include <libavutil/opt.h>
3236
#include <libavformat/avformat.h>
3337
#include <libswscale/swscale.h>
3438

@@ -342,9 +346,74 @@ static void test_close_shared_ffmpeg_with_allocated_resources(void **state)
342346
}
343347

344348
/* ==========================================================================
345-
* close_ffmpeg_source — use_ffmpeg=true with allocated resources
349+
* ffmpeg_resolve_sws_threads
346350
* ========================================================================== */
347351

352+
static void test_resolve_sws_threads_auto_within_bounds(void **state)
353+
{
354+
(void)state;
355+
/* The result must be usable as a libswscale thread count regardless of
356+
* the host CPU count. */
357+
int t = ffmpeg_resolve_sws_threads();
358+
assert_true(t >= 1);
359+
assert_true(t <= 8);
360+
}
361+
362+
static void test_resolve_sws_threads_auto_is_deterministic(void **state)
363+
{
364+
(void)state;
365+
assert_int_equal(ffmpeg_resolve_sws_threads(),
366+
ffmpeg_resolve_sws_threads());
367+
}
368+
369+
/* ==========================================================================
370+
* close_shared_ffmpeg — reference-counted yuv_frame (av_frame_get_buffer)
371+
* ========================================================================== */
372+
373+
/* The threaded conversion path requires yuv_frame to own an AVBuffer.
374+
* close_ffmpeg_decoder must then let av_frame_free() release the storage
375+
* instead of calling av_freep(&data[0]) on it, which would double-free. */
376+
static void test_close_shared_ffmpeg_with_refcounted_yuv_frame(void **state)
377+
{
378+
(void)state;
379+
struct shared_decode_ctx dec;
380+
memset(&dec, 0, sizeof(dec));
381+
382+
dec.av_frame = av_frame_alloc();
383+
dec.av_packet = av_packet_alloc();
384+
385+
dec.yuv_frame = av_frame_alloc();
386+
dec.yuv_frame->format = AV_PIX_FMT_YUV444P12LE;
387+
dec.yuv_frame->width = 64;
388+
dec.yuv_frame->height = 32;
389+
assert_int_equal(av_frame_get_buffer(dec.yuv_frame, 32), 0);
390+
assert_non_null(dec.yuv_frame->buf[0]);
391+
392+
/* Built the same way ffmpeg_decoder.c builds it, so the threaded
393+
* sub-contexts are exercised by the free path too. */
394+
dec.sws_ctx = sws_alloc_context();
395+
assert_non_null(dec.sws_ctx);
396+
av_opt_set_int(dec.sws_ctx, "srcw", 64, 0);
397+
av_opt_set_int(dec.sws_ctx, "srch", 32, 0);
398+
av_opt_set_int(dec.sws_ctx, "src_format", AV_PIX_FMT_YUV420P, 0);
399+
av_opt_set_int(dec.sws_ctx, "dstw", 64, 0);
400+
av_opt_set_int(dec.sws_ctx, "dsth", 32, 0);
401+
av_opt_set_int(dec.sws_ctx, "dst_format", AV_PIX_FMT_YUV444P12LE, 0);
402+
av_opt_set_int(dec.sws_ctx, "sws_flags", SWS_FAST_BILINEAR, 0);
403+
av_opt_set_int(dec.sws_ctx, "threads", 4, 0);
404+
assert_true(sws_init_context(dec.sws_ctx, NULL, NULL) >= 0);
405+
406+
close_shared_ffmpeg(&dec);
407+
408+
assert_null(dec.av_frame);
409+
assert_null(dec.yuv_frame);
410+
assert_null(dec.av_packet);
411+
assert_null(dec.sws_ctx);
412+
}
413+
414+
/* ==========================================================================
415+
* close_ffmpeg_source — use_ffmpeg=true with allocated resources
416+
* ========================================================================== */
348417
static void test_close_ffmpeg_source_with_allocated_resources(void **state)
349418
{
350419
(void)state;
@@ -441,6 +510,9 @@ int main(void)
441510
/* --- close_shared_ffmpeg --- */
442511
cmocka_unit_test(test_close_shared_ffmpeg_all_null_no_crash),
443512
cmocka_unit_test(test_close_shared_ffmpeg_with_allocated_resources),
513+
cmocka_unit_test(test_close_shared_ffmpeg_with_refcounted_yuv_frame),
514+
cmocka_unit_test(test_resolve_sws_threads_auto_within_bounds),
515+
cmocka_unit_test(test_resolve_sws_threads_auto_is_deterministic),
444516

445517
/* --- ffmpeg_decode_next_frame --- */
446518
cmocka_unit_test(test_ffmpeg_decode_next_frame_null_ctx_returns_false),

0 commit comments

Comments
 (0)