From: sanks011 via ffmpeg-devel <ffmpeg-devel@ffmpeg.org>
To: ffmpeg-devel@ffmpeg.org
Cc: sanks011 <code@ffmpeg.org>
Subject: [FFmpeg-devel] [PR] fate: add tests for hlsenc, cbs_sei, and timecode to improve coverage (PR #22282)
Date: Wed, 25 Feb 2026 14:44:29 -0000
Message-ID: <177203066956.25.4466026026922548274@29965ddac10e> (raw)
PR #22282 opened by sanks011
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22282
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22282.patch
This patch series adds FATE test coverage for three under-tested files
from three different libraries, as part of task of bringing low-coverage files above 90% line coverage.
## libavformat/hlsenc.c
Baseline coverage: <50% lines
Added 11 new FATE integration tests in `tests/fate/hlsenc.mak` covering
previously untested HLS muxer code paths:
- `hls_flags`: `round_durations`, `split_by_time`, `discont_start`,
`independent_segments`, `delete_segments`, `temp_file`
- Options: `start_number`, `hls_base_url`, `allow_cache`
- Playlist types: `event`, `vod`
## libavcodec/cbs_sei.c
Baseline coverage: **36.8% lines, 29.2% branches** (2026-02-25)
Added `libavcodec/tests/cbs_sei.c` — the first dedicated unit test for
this file, covering the complete public API:
- `ff_cbs_sei_find_type` — common + codec-specific types, unknown type
- `ff_cbs_sei_alloc_message_payload` — plain, user_data_registered, user_data_unregistered
- `ff_cbs_sei_list_add` + `ff_cbs_sei_free_message_list`
- `ff_cbs_sei_add_message`, `ff_cbs_sei_find_message`, `ff_cbs_sei_delete_message_type`
Tests run with H.264, H.265, and H.266 CBS contexts to cover all
branches in `cbs_sei_get_unit` and `cbs_sei_get_message_list` (including
the H.265 suffix SEI path). H.265/H.266 contexts are initialised
optionally and skipped gracefully if not compiled in.
Registered in `libavcodec/Makefile` (gated on `CONFIG_CBS_H264`) and
`tests/fate/libavcodec.mak`.
## libavutil/timecode.c
Baseline coverage: **86.2% lines, 71.4% branches** — no dedicated test existed
Added `libavutil/tests/timecode.c` — the first direct unit test for
`timecode.c`, covering all public API functions:
- `av_timecode_init`, `av_timecode_init_from_components`, `av_timecode_init_from_string`
- `av_timecode_make_string`, `av_timecode_make_smpte_tc_string{,2}`, `av_timecode_make_mpeg_tc_string`
- `av_timecode_get_smpte_from_framenum`, `av_timecode_get_smpte`
- `av_timecode_adjust_ntsc_framenum2`, `av_timecode_check_frame_rate`
Includes drop-frame and non-drop-frame paths, 24h wrap, NTSC adjustment
for 30/60fps, and error path validation.
Registered in `libavutil/Makefile` and `tests/fate/libavutil.mak`.
From 6451c22d38c9c48ae0cfbb2acfcd549f580dbd4b Mon Sep 17 00:00:00 2001
From: Sankalpa Sarkar <sankalpasarkar68@gmail.com>
Date: Wed, 25 Feb 2026 20:01:17 +0530
Subject: [PATCH] Add unit tests for CBS SEI handling and timecode utilities
- Introduced a new test file for CBS SEI functions in libavcodec, covering
functionalities such as finding SEI types, adding messages, and managing
message lists.
- Added a new test file for timecode utilities in libavutil, testing
initialization, string conversion, and SMPTE formatting.
- Updated fate test configurations to include the new tests for CBS SEI and
timecode functionalities.
---
libavcodec/Makefile | 1 +
libavcodec/tests/cbs_sei.c | 666 +++++++++++++++++++++++++++++++++++++
libavutil/Makefile | 1 +
libavutil/tests/timecode.c | 385 +++++++++++++++++++++
tests/fate/hlsenc.mak | 184 ++++++++++
tests/fate/libavcodec.mak | 5 +
tests/fate/libavutil.mak | 5 +
7 files changed, 1247 insertions(+)
create mode 100644 libavcodec/tests/cbs_sei.c
create mode 100644 libavutil/tests/timecode.c
diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 7d3e4aa1b4..74a995b256 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1378,6 +1378,7 @@ TESTPROGS-$(CONFIG_DXV_ENCODER) += hashtable
TESTPROGS-$(CONFIG_MJPEG_ENCODER) += mjpegenc_huffman
TESTPROGS-$(HAVE_MMX) += motion
TESTPROGS-$(CONFIG_MPEGVIDEO) += mpeg12framerate
+TESTPROGS-$(CONFIG_CBS_H264) += cbs_sei
TESTPROGS-$(CONFIG_H264_METADATA_BSF) += h264_levels
TESTPROGS-$(CONFIG_HEVC_METADATA_BSF) += h265_levels
TESTPROGS-$(CONFIG_RANGECODER) += rangecoder
diff --git a/libavcodec/tests/cbs_sei.c b/libavcodec/tests/cbs_sei.c
new file mode 100644
index 0000000000..c8b4afe98b
--- /dev/null
+++ b/libavcodec/tests/cbs_sei.c
@@ -0,0 +1,666 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Unit tests for cbs_sei.c functions:
+ * ff_cbs_sei_find_type, ff_cbs_sei_alloc_message_payload,
+ * ff_cbs_sei_list_add, ff_cbs_sei_free_message_list,
+ * ff_cbs_sei_add_message, ff_cbs_sei_find_message,
+ * ff_cbs_sei_delete_message_type
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/log.h"
+#include "libavutil/macros.h"
+#include "libavcodec/cbs.h"
+#include "libavcodec/cbs_h264.h"
+#include "libavcodec/cbs_sei.h"
+#include "libavcodec/sei.h"
+
+#define CHECK_RET(label, expr) do { \
+ int err__ = (expr); \
+ if (err__ < 0) { \
+ av_log(NULL, AV_LOG_ERROR, \
+ "%s failed: err=%d\n", \
+ label, err__); \
+ ret = 1; \
+ goto end; \
+ } \
+} while (0)
+
+/* ------------------------------------------------------------------ */
+/* Test ff_cbs_sei_find_type */
+/* ------------------------------------------------------------------ */
+static int test_find_type(CodedBitstreamContext *ctx)
+{
+ const SEIMessageTypeDescriptor *desc;
+
+ /* Known common type - filler payload */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_FILLER_PAYLOAD);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: expected descriptor for SEI_TYPE_FILLER_PAYLOAD\n");
+ return 1;
+ }
+ if (desc->type != SEI_TYPE_FILLER_PAYLOAD) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: wrong type returned: %d\n", desc->type);
+ return 1;
+ }
+
+ /* User-data registered */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_USER_DATA_REGISTERED_ITU_T_T35);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: expected descriptor for user_data_registered\n");
+ return 1;
+ }
+
+ /* User-data unregistered */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_USER_DATA_UNREGISTERED);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: expected descriptor for user_data_unregistered\n");
+ return 1;
+ }
+
+ /* Mastering display colour volume */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: expected descriptor for mastering_display_colour_volume\n");
+ return 1;
+ }
+
+ /* Content light level */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: expected descriptor for content_light_level\n");
+ return 1;
+ }
+
+ /* Unknown type - should return NULL */
+ desc = ff_cbs_sei_find_type(ctx, 0x7fff);
+ if (desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type: got unexpected descriptor for unknown type\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test ff_cbs_sei_list_add and ff_cbs_sei_free_message_list */
+/* ------------------------------------------------------------------ */
+static int test_list_add_and_free(void)
+{
+ SEIRawMessageList list = { 0 };
+ int ret = 0;
+
+ /* Add several messages and verify counter grows */
+ for (int i = 0; i < 5; i++) {
+ ret = ff_cbs_sei_list_add(&list);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "list_add: iteration %d failed\n", i);
+ goto done;
+ }
+ if (list.nb_messages != i + 1) {
+ av_log(NULL, AV_LOG_ERROR,
+ "list_add: expected %d messages, got %d\n",
+ i + 1, list.nb_messages);
+ ret = 1;
+ goto done;
+ }
+ }
+
+ /* Capacity must be >= nb_messages */
+ if (list.nb_messages_allocated < list.nb_messages) {
+ av_log(NULL, AV_LOG_ERROR,
+ "list_add: nb_messages_allocated %d < nb_messages %d\n",
+ list.nb_messages_allocated, list.nb_messages);
+ ret = 1;
+ goto done;
+ }
+
+done:
+ ff_cbs_sei_free_message_list(&list);
+ if (list.messages) {
+ av_log(NULL, AV_LOG_ERROR,
+ "free_message_list: messages pointer not NULLed\n");
+ return 1;
+ }
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test ff_cbs_sei_alloc_message_payload */
+/* ------------------------------------------------------------------ */
+static int test_alloc_message_payload(CodedBitstreamContext *ctx)
+{
+ const SEIMessageTypeDescriptor *desc;
+ SEIRawMessage msg = { 0 };
+ int ret;
+
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: descriptor not found\n");
+ return 1;
+ }
+
+ ret = ff_cbs_sei_alloc_message_payload(&msg, desc);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: allocation failed\n");
+ return 1;
+ }
+ if (!msg.payload || !msg.payload_ref) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: payload or payload_ref is NULL\n");
+ av_refstruct_unref(&msg.payload_ref);
+ return 1;
+ }
+ if (msg.payload_type != SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: wrong payload_type %u\n", msg.payload_type);
+ av_refstruct_unref(&msg.payload_ref);
+ return 1;
+ }
+
+ av_refstruct_unref(&msg.payload_ref);
+
+ /* Test with user_data_registered (has a special free function) */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_USER_DATA_REGISTERED_ITU_T_T35);
+ if (!desc)
+ return 1;
+ memset(&msg, 0, sizeof(msg));
+ ret = ff_cbs_sei_alloc_message_payload(&msg, desc);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: user_data_registered allocation failed\n");
+ return 1;
+ }
+ av_refstruct_unref(&msg.payload_ref);
+
+ /* Test with user_data_unregistered (has another special free function) */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_USER_DATA_UNREGISTERED);
+ if (!desc)
+ return 1;
+ memset(&msg, 0, sizeof(msg));
+ ret = ff_cbs_sei_alloc_message_payload(&msg, desc);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "alloc_payload: user_data_unregistered allocation failed\n");
+ return 1;
+ }
+ av_refstruct_unref(&msg.payload_ref);
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test ff_cbs_sei_add_message / find_message / delete_message_type */
+/* ------------------------------------------------------------------ */
+static int test_add_find_delete(CodedBitstreamContext *ctx)
+{
+ CodedBitstreamFragment au = { 0 };
+ SEIRawMessage *iter = NULL;
+ SEIRawContentLightLevelInfo *cll;
+ const SEIMessageTypeDescriptor *desc;
+ int ret = 0;
+
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR, "add_find_delete: descriptor not found\n");
+ return 1;
+ }
+
+ /* Add first message */
+ {
+ SEIRawMessage msg = { 0 };
+ CHECK_RET("alloc_payload", ff_cbs_sei_alloc_message_payload(&msg, desc));
+
+ cll = msg.payload;
+ cll->max_content_light_level = 1000;
+ cll->max_pic_average_light_level = 400;
+
+ ret = ff_cbs_sei_add_message(ctx, &au, 1,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ msg.payload, msg.payload_ref);
+ av_refstruct_unref(&msg.payload_ref);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: first add_message failed\n");
+ ret = 1;
+ goto end;
+ }
+ }
+
+ /* Add second message of the same type */
+ {
+ SEIRawMessage msg = { 0 };
+ CHECK_RET("alloc_payload 2", ff_cbs_sei_alloc_message_payload(&msg, desc));
+
+ cll = msg.payload;
+ cll->max_content_light_level = 2000;
+ cll->max_pic_average_light_level = 600;
+
+ ret = ff_cbs_sei_add_message(ctx, &au, 1,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ msg.payload, msg.payload_ref);
+ av_refstruct_unref(&msg.payload_ref);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: second add_message failed\n");
+ ret = 1;
+ goto end;
+ }
+ }
+
+ /* Find first message */
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret < 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: first find_message failed\n");
+ ret = 1;
+ goto end;
+ }
+ cll = iter->payload;
+ if (cll->max_content_light_level != 1000) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: wrong CLL value %u (expected 1000)\n",
+ cll->max_content_light_level);
+ ret = 1;
+ goto end;
+ }
+
+ /* Find second message using the iterator */
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret < 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: second find_message failed\n");
+ ret = 1;
+ goto end;
+ }
+ cll = iter->payload;
+ if (cll->max_content_light_level != 2000) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: wrong CLL value %u (expected 2000)\n",
+ cll->max_content_light_level);
+ ret = 1;
+ goto end;
+ }
+
+ /* No more messages */
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != AVERROR(ENOENT)) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: expected ENOENT for exhausted iterator\n");
+ ret = 1;
+ goto end;
+ }
+
+ /* Delete all messages of this type */
+ ff_cbs_sei_delete_message_type(ctx, &au, SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO);
+
+ /* Verify they are all gone */
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != AVERROR(ENOENT)) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete: messages remain after delete_message_type\n");
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ ff_cbs_fragment_free(&au);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test add_message without a refcount (payload_ref == NULL) */
+/* ------------------------------------------------------------------ */
+static int test_add_message_no_ref(CodedBitstreamContext *ctx)
+{
+ CodedBitstreamFragment au = { 0 };
+ SEIRawContentLightLevelInfo cll = {
+ .max_content_light_level = 500,
+ .max_pic_average_light_level = 200,
+ };
+ SEIRawMessage *iter = NULL;
+ int ret = 0;
+
+ ret = ff_cbs_sei_add_message(ctx, &au, 1,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ &cll, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_no_ref: add_message failed: %d\n", ret);
+ ret = 1;
+ goto end;
+ }
+
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_no_ref: find_message failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+end:
+ ff_cbs_fragment_free(&au);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test that find_message correctly iterates over multiple SEI units */
+/* ------------------------------------------------------------------ */
+static int test_find_message_across_units(CodedBitstreamContext *ctx)
+{
+ CodedBitstreamFragment au = { 0 };
+ SEIRawMasteringDisplayColourVolume mdcv = {
+ .display_primaries_x = { 13250, 7500, 34000 },
+ .display_primaries_y = { 34500, 3000, 16000 },
+ .white_point_x = 15635,
+ .white_point_y = 16450,
+ .max_display_mastering_luminance = 10000000,
+ .min_display_mastering_luminance = 50,
+ };
+ SEIRawContentLightLevelInfo cll = {
+ .max_content_light_level = 1000,
+ .max_pic_average_light_level = 400,
+ };
+ SEIRawMessage *iter = NULL;
+ int ret = 0;
+
+ /* Add two different SEI types */
+ ret = ff_cbs_sei_add_message(ctx, &au, 1,
+ SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME,
+ &mdcv, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: add mdcv failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ ret = ff_cbs_sei_add_message(ctx, &au, 1,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ &cll, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: add cll failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ /* Find mastering display */
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME, &iter);
+ if (ret != 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: find mdcv failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ /* Find content light level */
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: find cll failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ /* Try a type not present */
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_FILLER_PAYLOAD, &iter);
+ if (ret != AVERROR(ENOENT)) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: expected ENOENT for absent type\n");
+ ret = 1;
+ goto end;
+ }
+
+ /* Delete one type and ensure the other remains */
+ ff_cbs_sei_delete_message_type(ctx, &au,
+ SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME);
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "multi_unit: cll gone after deleting unrelated type\n");
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+end:
+ ff_cbs_fragment_free(&au);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test find_type for H.264-specific types */
+/* ------------------------------------------------------------------ */
+static int test_find_type_h264(CodedBitstreamContext *ctx)
+{
+ const SEIMessageTypeDescriptor *desc;
+
+ /* Buffering period is H.264-specific */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_BUFFERING_PERIOD);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type_h264: expected descriptor for buffering_period\n");
+ return 1;
+ }
+
+ /* Decoded picture hash is a common suffix type */
+ desc = ff_cbs_sei_find_type(ctx, SEI_TYPE_DECODED_PICTURE_HASH);
+ if (!desc) {
+ av_log(NULL, AV_LOG_ERROR,
+ "find_type_h264: expected descriptor for decoded_picture_hash\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test add/find/delete using a given CBS context (codec-agnostic). */
+/* Used for H.265 and H.266 to cover those branches in */
+/* cbs_sei_get_unit / cbs_sei_get_message_list. */
+/* prefix=1 for H.264/H.265; H.266 uses prefix SEI as well. */
+/* ------------------------------------------------------------------ */
+static int test_add_find_delete_codec(CodedBitstreamContext *ctx, int prefix,
+ const char *codec_name)
+{
+ CodedBitstreamFragment au = { 0 };
+ SEIRawContentLightLevelInfo cll = {
+ .max_content_light_level = 1000,
+ .max_pic_average_light_level = 400,
+ };
+ SEIRawMessage *iter = NULL;
+ int ret = 0;
+
+ /* Add a message */
+ ret = ff_cbs_sei_add_message(ctx, &au, prefix,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ &cll, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete_%s: add_message failed: %d\n", codec_name, ret);
+ ret = 1;
+ goto end;
+ }
+
+ /* Find it */
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret < 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete_%s: find_message failed\n", codec_name);
+ ret = 1;
+ goto end;
+ }
+
+ /* Delete it */
+ ff_cbs_sei_delete_message_type(ctx, &au, SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO);
+
+ /* Verify gone */
+ iter = NULL;
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret != AVERROR(ENOENT)) {
+ av_log(NULL, AV_LOG_ERROR,
+ "add_find_delete_%s: expected ENOENT after delete\n", codec_name);
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+end:
+ ff_cbs_fragment_free(&au);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* Test H.265 suffix SEI unit (prefix=0 path in cbs_sei_get_unit) */
+/* ------------------------------------------------------------------ */
+static int test_h265_suffix_sei(CodedBitstreamContext *ctx)
+{
+ CodedBitstreamFragment au = { 0 };
+ SEIRawContentLightLevelInfo cll = {
+ .max_content_light_level = 500,
+ .max_pic_average_light_level = 200,
+ };
+ SEIRawMessage *iter = NULL;
+ int ret = 0;
+
+ /* prefix=0 → suffix SEI (HEVC_NAL_SEI_SUFFIX path) */
+ ret = ff_cbs_sei_add_message(ctx, &au, 0,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
+ &cll, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR,
+ "h265_suffix: add_message failed: %d\n", ret);
+ ret = 1;
+ goto end;
+ }
+
+ ret = ff_cbs_sei_find_message(ctx, &au,
+ SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO, &iter);
+ if (ret < 0 || !iter) {
+ av_log(NULL, AV_LOG_ERROR,
+ "h265_suffix: find_message failed\n");
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+end:
+ ff_cbs_fragment_free(&au);
+ return ret;
+}
+
+/* ------------------------------------------------------------------ */
+/* main */
+/* ------------------------------------------------------------------ */
+int main(void)
+{
+ CodedBitstreamContext *ctx264 = NULL;
+ CodedBitstreamContext *ctx265 = NULL;
+ CodedBitstreamContext *ctx266 = NULL;
+ int ret = 0;
+ int failed = 0;
+
+#define RUN_TEST(name, ...) do { \
+ int r = name(__VA_ARGS__); \
+ if (r) { \
+ av_log(NULL, AV_LOG_ERROR, "FAIL: %s\n", #name); \
+ failed++; \
+ } \
+} while (0)
+
+ /* H.264 context — required */
+ ret = ff_cbs_init(&ctx264, AV_CODEC_ID_H264, NULL);
+ if (ret < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Failed to init H264 CBS context: %d\n", ret);
+ return 1;
+ }
+
+ /* H.265 context — optional (skip gracefully if not compiled in) */
+ if (ff_cbs_init(&ctx265, AV_CODEC_ID_H265, NULL) < 0)
+ ctx265 = NULL;
+
+ /* H.266 context — optional */
+ if (ff_cbs_init(&ctx266, AV_CODEC_ID_H266, NULL) < 0)
+ ctx266 = NULL;
+
+ /* -- H.264 tests -- */
+ RUN_TEST(test_find_type, ctx264);
+ RUN_TEST(test_find_type_h264, ctx264);
+ RUN_TEST(test_list_add_and_free);
+ RUN_TEST(test_alloc_message_payload, ctx264);
+ RUN_TEST(test_add_find_delete, ctx264);
+ RUN_TEST(test_add_message_no_ref, ctx264);
+ RUN_TEST(test_find_message_across_units, ctx264);
+
+ /* -- H.265 tests: cover H265 branches in cbs_sei_get_unit /
+ cbs_sei_get_message_list (skipped if H.265 CBS not compiled in) -- */
+ if (ctx265) {
+ RUN_TEST(test_add_find_delete_codec, ctx265, 1, "h265_prefix");
+ RUN_TEST(test_h265_suffix_sei, ctx265);
+ }
+
+ /* -- H.266 tests: cover H266 branches (skipped if not compiled in) -- */
+ if (ctx266)
+ RUN_TEST(test_add_find_delete_codec, ctx266, 1, "h266_prefix");
+
+ ff_cbs_close(&ctx264);
+ ff_cbs_close(&ctx265);
+ ff_cbs_close(&ctx266);
+
+ if (failed) {
+ av_log(NULL, AV_LOG_ERROR, "%d test(s) FAILED\n", failed);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/libavutil/Makefile b/libavutil/Makefile
index c5241895ff..2c0dfa4e4a 100644
--- a/libavutil/Makefile
+++ b/libavutil/Makefile
@@ -301,6 +301,7 @@ TESTPROGS = adler32 \
side_data_array \
softfloat \
tree \
+ timecode \
twofish \
utf8 \
uuid \
diff --git a/libavutil/tests/timecode.c b/libavutil/tests/timecode.c
new file mode 100644
index 0000000000..cefa811bd2
--- /dev/null
+++ b/libavutil/tests/timecode.c
@@ -0,0 +1,385 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Unit tests for libavutil/timecode.c:
+ * av_timecode_init, av_timecode_init_from_components,
+ * av_timecode_init_from_string, av_timecode_make_string,
+ * av_timecode_get_smpte_from_framenum, av_timecode_get_smpte,
+ * av_timecode_make_smpte_tc_string, av_timecode_make_smpte_tc_string2,
+ * av_timecode_make_mpeg_tc_string, av_timecode_adjust_ntsc_framenum2,
+ * av_timecode_check_frame_rate
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/error.h"
+#include "libavutil/macros.h"
+#include "libavutil/rational.h"
+#include "libavutil/timecode.h"
+
+static int failed;
+
+#define ASSERT_EQ(got, exp, label) do { \
+ if ((got) != (exp)) { \
+ fprintf(stderr, "FAIL %s: got %d, expected %d\n", \
+ label, (int)(got), (int)(exp)); \
+ failed++; \
+ } \
+} while (0)
+
+#define ASSERT_STR(got, exp, label) do { \
+ if (strcmp(got, exp)) { \
+ fprintf(stderr, "FAIL %s: got '%s', expected '%s'\n", \
+ label, got, exp); \
+ failed++; \
+ } \
+} while (0)
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_check_frame_rate */
+/* ----------------------------------------------------------------------- */
+static void test_check_frame_rate(void)
+{
+ /* Standard rates must return 0 */
+ static const struct { AVRational rate; int expect; } cases[] = {
+ { {24, 1}, 0 },
+ { {25, 1}, 0 },
+ { {30, 1}, 0 },
+ { {48, 1}, 0 },
+ { {50, 1}, 0 },
+ { {60, 1}, 0 },
+ { {100, 1}, 0 },
+ { {120, 1}, 0 },
+ /* Non-standard → should return non-zero warning */
+ { {23976, 1000}, -1 },
+ { {29970, 1000}, -1 },
+ /* Zero / invalid denominator */
+ { {0, 0}, -1 },
+ { {30, 0}, -1 },
+ };
+ for (int i = 0; i < FF_ARRAY_ELEMS(cases); i++) {
+ int r = av_timecode_check_frame_rate(cases[i].rate);
+ if (cases[i].expect == 0) {
+ ASSERT_EQ(r, 0, "check_frame_rate standard");
+ } else {
+ if (r == 0) {
+ fprintf(stderr,
+ "FAIL check_frame_rate: non-standard %d/%d returned 0\n",
+ cases[i].rate.num, cases[i].rate.den);
+ failed++;
+ }
+ }
+ }
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_init */
+/* ----------------------------------------------------------------------- */
+static void test_init(void)
+{
+ AVTimecode tc;
+ int ret;
+
+ /* Normal 25 fps */
+ ret = av_timecode_init(&tc, (AVRational){25, 1}, 0, 0, NULL);
+ ASSERT_EQ(ret, 0, "init 25fps");
+ ASSERT_EQ(tc.fps, 25, "init fps");
+ ASSERT_EQ(tc.start, 0, "init start");
+ ASSERT_EQ(tc.rate.num, 25, "init rate.num");
+ ASSERT_EQ(tc.rate.den, 1, "init rate.den");
+
+ /* 30 fps with DROPFRAME is invalid for non-multiple-of-30000/1001 rates */
+ ret = av_timecode_init(&tc, (AVRational){30, 1},
+ AV_TIMECODE_FLAG_DROPFRAME, 0, NULL);
+ /* 30 fps IS a multiple of 30 → dropframe is allowed */
+ ASSERT_EQ(ret, 0, "init 30fps dropframe");
+
+ /* frame_start propagated */
+ ret = av_timecode_init(&tc, (AVRational){24, 1}, 0, 100, NULL);
+ ASSERT_EQ(ret, 0, "init with start");
+ ASSERT_EQ(tc.start, 100, "init start propagated");
+
+ /* Invalid: zero rate */
+ ret = av_timecode_init(&tc, (AVRational){0, 1}, 0, 0, NULL);
+ if (ret >= 0) {
+ fprintf(stderr, "FAIL init: zero fps should fail, got ret=%d\n", ret);
+ failed++;
+ }
+
+ /* Invalid: drop frame with non-multiple-of-30 fps */
+ ret = av_timecode_init(&tc, (AVRational){25, 1},
+ AV_TIMECODE_FLAG_DROPFRAME, 0, NULL);
+ if (ret >= 0) {
+ fprintf(stderr,
+ "FAIL init: drop+25fps should fail, got ret=%d\n", ret);
+ failed++;
+ }
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_init_from_components */
+/* ----------------------------------------------------------------------- */
+static void test_init_from_components(void)
+{
+ AVTimecode tc;
+ int ret;
+
+ /* 00:00:00:00 at 25fps → start = 0 */
+ ret = av_timecode_init_from_components(&tc, (AVRational){25, 1},
+ 0, 0, 0, 0, 0, NULL);
+ ASSERT_EQ(ret, 0, "init_from_components 00:00:00:00");
+ ASSERT_EQ(tc.start, 0, "components start 0");
+
+ /* 01:00:00:00 at 25fps → start = 3600 * 25 = 90000 */
+ ret = av_timecode_init_from_components(&tc, (AVRational){25, 1},
+ 0, 1, 0, 0, 0, NULL);
+ ASSERT_EQ(ret, 0, "init_from_components 01:00:00:00");
+ ASSERT_EQ(tc.start, 90000, "components start 01:00:00:00");
+
+ /* 01:02:03:04 at 30fps → 1*3600*30 + 2*60*30 + 3*30 + 4 = 111694 */
+ ret = av_timecode_init_from_components(&tc, (AVRational){30, 1},
+ 0, 1, 2, 3, 4, NULL);
+ ASSERT_EQ(ret, 0, "init_from_components 01:02:03:04");
+ ASSERT_EQ(tc.start, 111694, "components start 01:02:03:04");
+
+ /* Drop frame 29.97: start should be adjusted for dropped frames */
+ ret = av_timecode_init_from_components(&tc, (AVRational){30000, 1001},
+ AV_TIMECODE_FLAG_DROPFRAME,
+ 1, 0, 0, 0, NULL);
+ ASSERT_EQ(ret, 0, "init_from_components drop 01:00:00;00");
+ /* 1h at 29.97 drop = 107892 frames (standard drop-frame count) */
+ ASSERT_EQ(tc.start, 107892, "components drop start 01h");
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_init_from_string */
+/* ----------------------------------------------------------------------- */
+static void test_init_from_string(void)
+{
+ AVTimecode tc;
+ int ret;
+
+ /* Non-drop ":" separator */
+ ret = av_timecode_init_from_string(&tc, (AVRational){30, 1},
+ "00:01:02:03", NULL);
+ ASSERT_EQ(ret, 0, "init_from_string non-drop");
+ ASSERT_EQ(!!(tc.flags & AV_TIMECODE_FLAG_DROPFRAME), 0, "string non-drop flag");
+ /* 0*3600*30 + 1*60*30 + 2*30 + 3 = 1863 */
+ ASSERT_EQ(tc.start, 1863, "string non-drop start");
+
+ /* Drop ";" separator */
+ ret = av_timecode_init_from_string(&tc, (AVRational){30000, 1001},
+ "00:01:00;02", NULL);
+ ASSERT_EQ(ret, 0, "init_from_string drop");
+ ASSERT_EQ(!!(tc.flags & AV_TIMECODE_FLAG_DROPFRAME), 1, "string drop flag");
+
+ /* Dot '.' separator also means drop */
+ ret = av_timecode_init_from_string(&tc, (AVRational){30000, 1001},
+ "01:00:00.00", NULL);
+ ASSERT_EQ(ret, 0, "init_from_string dot-drop");
+ ASSERT_EQ(!!(tc.flags & AV_TIMECODE_FLAG_DROPFRAME), 1, "string dot-drop flag");
+
+ /* Invalid string */
+ ret = av_timecode_init_from_string(&tc, (AVRational){25, 1},
+ "notvalid", NULL);
+ if (ret >= 0) {
+ fprintf(stderr, "FAIL init_from_string: invalid str should fail\n");
+ failed++;
+ }
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_make_string */
+/* ----------------------------------------------------------------------- */
+static void test_make_string(void)
+{
+ AVTimecode tc;
+ char buf[AV_TIMECODE_STR_SIZE];
+
+ /* 25fps, no flags: "00:00:01:10" for frame 35 (= 1s*25 + 10) */
+ av_timecode_init(&tc, (AVRational){25, 1}, 0, 0, NULL);
+ av_timecode_make_string(&tc, buf, 35);
+ ASSERT_STR(buf, "00:00:01:10", "make_string 25fps frame 35");
+
+ /* frame 0 → "00:00:00:00" */
+ av_timecode_make_string(&tc, buf, 0);
+ ASSERT_STR(buf, "00:00:00:00", "make_string frame 0");
+
+ /* 30fps non-drop, frame 30 → "00:00:01:00" */
+ av_timecode_init(&tc, (AVRational){30, 1}, 0, 0, NULL);
+ av_timecode_make_string(&tc, buf, 30);
+ ASSERT_STR(buf, "00:00:01:00", "make_string 30fps frame 30");
+
+ /* AV_TIMECODE_FLAG_24HOURSMAX: hour wraps at 24 */
+ av_timecode_init(&tc, (AVRational){25, 1},
+ AV_TIMECODE_FLAG_24HOURSMAX, 0, NULL);
+ /* frame at 25h = 25*3600*25 = 2250000 frames, should wrap to 01:00:00:00 */
+ av_timecode_make_string(&tc, buf, 25 * 3600 * 25);
+ ASSERT_STR(buf, "01:00:00:00", "make_string 24h wrap");
+
+ /* Drop-frame 29.97: separator should be ';' */
+ av_timecode_init(&tc, (AVRational){30000, 1001},
+ AV_TIMECODE_FLAG_DROPFRAME, 0, NULL);
+ av_timecode_make_string(&tc, buf, 0);
+ if (strchr(buf, ';') == NULL) {
+ fprintf(stderr, "FAIL make_string: drop frame should use ';', got '%s'\n", buf);
+ failed++;
+ }
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_make_smpte_tc_string */
+/* ----------------------------------------------------------------------- */
+static void test_make_smpte_tc_string(void)
+{
+ char buf[AV_TIMECODE_STR_SIZE];
+ AVTimecode tc;
+ uint32_t smpte;
+
+ /* Build SMPTE code for 01:02:03:04 at 30fps non-drop */
+ smpte = av_timecode_get_smpte((AVRational){30, 1}, 0, 1, 2, 3, 4);
+ av_timecode_make_smpte_tc_string(buf, smpte, 1);
+ ASSERT_STR(buf, "01:02:03:04", "smpte_tc_string 01:02:03:04");
+
+ /* Using av_timecode_get_smpte_from_framenum */
+ av_timecode_init(&tc, (AVRational){25, 1}, 0, 0, NULL);
+ /* frame 25*3600 + 25*60 + 25*1 + 5 = 91530 → 01:01:01:05 at 25fps */
+ smpte = av_timecode_get_smpte_from_framenum(&tc, 25 * 3600 + 25 * 60 + 25 + 5);
+ av_timecode_make_smpte_tc_string(buf, smpte, 1);
+ ASSERT_STR(buf, "01:01:01:05", "smpte_from_framenum 01:01:01:05");
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_make_smpte_tc_string2 */
+/* ----------------------------------------------------------------------- */
+static void test_make_smpte_tc_string2(void)
+{
+ char buf[AV_TIMECODE_STR_SIZE];
+ uint32_t smpte;
+
+ /* 50fps: frame 0 should parse to "00:00:00:00" */
+ smpte = av_timecode_get_smpte((AVRational){50, 1}, 0, 0, 0, 0, 0);
+ av_timecode_make_smpte_tc_string2(buf, (AVRational){50, 1}, smpte, 1, 0);
+ ASSERT_STR(buf, "00:00:00:00", "smpte_tc_string2 50fps 00:00:00:00");
+
+ /* 60fps at 01:00:00:00 */
+ smpte = av_timecode_get_smpte((AVRational){60, 1}, 0, 1, 0, 0, 0);
+ av_timecode_make_smpte_tc_string2(buf, (AVRational){60, 1}, smpte, 1, 0);
+ ASSERT_STR(buf, "01:00:00:00", "smpte_tc_string2 60fps 01:00:00:00");
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_make_mpeg_tc_string */
+/* ----------------------------------------------------------------------- */
+static void test_make_mpeg_tc_string(void)
+{
+ char buf[AV_TIMECODE_STR_SIZE];
+
+ /* Build 25-bit MPEG timecode manually for 01:02:03:04 non-drop:
+ * hh=1, mm=2, ss=3, ff=4
+ * tc25bit = (hh<<19)|(mm<<13)|(ss<<6)|ff */
+ uint32_t tc25 = (1u << 19) | (2u << 13) | (3u << 6) | 4u;
+ av_timecode_make_mpeg_tc_string(buf, tc25);
+ ASSERT_STR(buf, "01:02:03:04", "mpeg_tc_string 01:02:03:04");
+
+ /* Drop flag bit 24=1 → separator ';' */
+ uint32_t tc25_drop = tc25 | (1u << 24);
+ av_timecode_make_mpeg_tc_string(buf, tc25_drop);
+ if (strchr(buf, ';') == NULL) {
+ fprintf(stderr,
+ "FAIL mpeg_tc_string: drop flag should produce ';', got '%s'\n", buf);
+ failed++;
+ }
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_adjust_ntsc_framenum2 */
+/* ----------------------------------------------------------------------- */
+static void test_adjust_ntsc(void)
+{
+ /* 29.97 drop: frame 0 → 0 */
+ ASSERT_EQ(av_timecode_adjust_ntsc_framenum2(0, 30), 0, "ntsc_adjust frame 0");
+
+ /* Frame 1800 (1 min at 30fps) → subtract 2 (first minute skip) */
+ ASSERT_EQ(av_timecode_adjust_ntsc_framenum2(1800, 30), 1800 + 2,
+ "ntsc_adjust 1800 (1min at 30)");
+
+ /* Non-multiple of 30 → unchanged */
+ ASSERT_EQ(av_timecode_adjust_ntsc_framenum2(1000, 25), 1000,
+ "ntsc_adjust non-30 fps");
+ ASSERT_EQ(av_timecode_adjust_ntsc_framenum2(1000, 0), 1000,
+ "ntsc_adjust fps=0");
+
+ /* 60fps (2x30): drop_frames=4, frames_per_10mins=35964 */
+ int adj = av_timecode_adjust_ntsc_framenum2(3600, 60);
+ /* 3600 frames into 1 min at 60fps → +4 skipped */
+ ASSERT_EQ(adj, 3600 + 4, "ntsc_adjust 60fps 1min");
+}
+
+/* ----------------------------------------------------------------------- */
+/* av_timecode_get_smpte roundtrip */
+/* ----------------------------------------------------------------------- */
+static void test_get_smpte_roundtrip(void)
+{
+ /* Encode and decode 12:34:56:07 at 30fps, check the string matches */
+ char buf[AV_TIMECODE_STR_SIZE];
+ uint32_t smpte = av_timecode_get_smpte((AVRational){30, 1}, 0, 12, 34, 56, 7);
+ av_timecode_make_smpte_tc_string(buf, smpte, 1);
+ ASSERT_STR(buf, "12:34:56:07", "smpte roundtrip 12:34:56:07");
+
+ /* 00:00:00:00 */
+ smpte = av_timecode_get_smpte((AVRational){30, 1}, 0, 0, 0, 0, 0);
+ av_timecode_make_smpte_tc_string(buf, smpte, 1);
+ ASSERT_STR(buf, "00:00:00:00", "smpte roundtrip 00:00:00:00");
+
+ /* Drop-frame bit */
+ smpte = av_timecode_get_smpte((AVRational){30000, 1001}, 1, 0, 1, 0, 2);
+ if (!(smpte & (1u << 30))) {
+ fprintf(stderr, "FAIL get_smpte: drop bit not set\n");
+ failed++;
+ }
+ av_timecode_make_smpte_tc_string(buf, smpte, 0 /* allow drop */);
+ /* separator should be ';' for drop */
+ if (strchr(buf, ';') == NULL) {
+ fprintf(stderr,
+ "FAIL get_smpte: drop roundtrip missing ';', got '%s'\n", buf);
+ failed++;
+ }
+}
+
+int main(void)
+{
+ test_check_frame_rate();
+ test_init();
+ test_init_from_components();
+ test_init_from_string();
+ test_make_string();
+ test_make_smpte_tc_string();
+ test_make_smpte_tc_string2();
+ test_make_mpeg_tc_string();
+ test_adjust_ntsc();
+ test_get_smpte_roundtrip();
+
+ if (failed) {
+ fprintf(stderr, "%d test(s) FAILED\n", failed);
+ return 1;
+ }
+ return 0;
+}
diff --git a/tests/fate/hlsenc.mak b/tests/fate/hlsenc.mak
index 2c4097d0d9..b0f42817e7 100644
--- a/tests/fate/hlsenc.mak
+++ b/tests/fate/hlsenc.mak
@@ -129,3 +129,187 @@ fate-hls-cmfa: CMD = framecrc -i $(TARGET_PATH)/tests/data/hls_cmfa.m3u8 -c copy
FATE_SAMPLES_FFMPEG += $(FATE_HLSENC-yes)
FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_HLSENC_PROBE-yes)
fate-hlsenc: $(FATE_HLSENC-yes) $(FATE_HLSENC_PROBE-yes)
+
+# ---------------------------------------------------------------------------
+# Additional tests for improved code coverage
+# ---------------------------------------------------------------------------
+
+# round_durations flag: exercises hls_window() HLS version 2 path and
+# rounded EXTINF writing via ff_hls_write_file_entry(round_durations=1).
+tests/data/hls_round_durations.m3u8: TAG = GEN
+tests/data/hls_round_durations.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 0 -hls_flags round_durations \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_round_durations_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_round_durations.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-round-durations
+fate-hls-round-durations: tests/data/hls_round_durations.m3u8
+fate-hls-round-durations: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_round_durations.m3u8 -vf setpts=N*23
+
+# split_by_time flag: exercises can_split path triggered by wall-clock time
+# rather than keyframe boundary in hls_write_packet().
+tests/data/hls_split_by_time.m3u8: TAG = GEN
+tests/data/hls_split_by_time.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 0 -hls_flags split_by_time \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_split_by_time_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_split_by_time.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-split-by-time
+fate-hls-split-by-time: tests/data/hls_split_by_time.m3u8
+fate-hls-split-by-time: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_split_by_time.m3u8 -vf setpts=N*23
+
+# discont_start flag: exercises the opening #EXT-X-DISCONTINUITY tag in
+# hls_window() and the HLS_DISCONT_START branch.
+tests/data/hls_discont_start.m3u8: TAG = GEN
+tests/data/hls_discont_start.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 4 -hls_list_size 0 -hls_flags discont_start \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_discont_start_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_discont_start.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-discont-start
+fate-hls-discont-start: tests/data/hls_discont_start.m3u8
+fate-hls-discont-start: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_discont_start.m3u8 -vf setpts=N*23
+
+# independent_segments flag: exercises the #EXT-X-INDEPENDENT-SEGMENTS line in
+# hls_window(), pushes playlist to version 6.
+tests/data/hls_independent_segs.m3u8: TAG = GEN
+tests/data/hls_independent_segs.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 0 -hls_flags independent_segments \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_independent_segs_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_independent_segs.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-independent-segs
+fate-hls-independent-segs: tests/data/hls_independent_segs.m3u8
+fate-hls-independent-segs: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_independent_segs.m3u8 -vf setpts=N*23
+
+# EVENT playlist type: exercises hls->pl_type == PLAYLIST_TYPE_EVENT branch,
+# disables sliding window (max_nb_segments=0) inside hls_append_segment().
+tests/data/hls_event_playlist.m3u8: TAG = GEN
+tests/data/hls_event_playlist.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 5 -hls_playlist_type event \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_event_playlist_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_event_playlist.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-event-playlist
+fate-hls-event-playlist: tests/data/hls_event_playlist.m3u8
+fate-hls-event-playlist: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_event_playlist.m3u8 -vf setpts=N*23
+
+# VOD playlist type: exercises hls->pl_type == PLAYLIST_TYPE_VOD, writes
+# #EXT-X-PLAYLIST-TYPE:VOD once only at the very end.
+tests/data/hls_vod.m3u8: TAG = GEN
+tests/data/hls_vod.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 0 -hls_playlist_type vod \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_vod_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_vod.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-vod
+fate-hls-vod: tests/data/hls_vod.m3u8
+fate-hls-vod: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_vod.m3u8 -vf setpts=N*23
+
+# delete_segments flag: exercises hls_delete_old_segments() which is called
+# from hls_append_segment() every time hls_list_size is exceeded.
+tests/data/hls_delete_segs.m3u8: TAG = GEN
+tests/data/hls_delete_segs.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=20" -f hls \
+ -hls_time 3 -hls_list_size 3 -hls_flags delete_segments \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_delete_segs_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_delete_segs.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-delete-segs
+fate-hls-delete-segs: tests/data/hls_delete_segs.m3u8
+fate-hls-delete-segs: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_delete_segs.m3u8 -vf setpts=N*23
+
+# start_number option: exercises the hls->start_sequence path in hls_init()
+# and the sequence initialisation in each VariantStream.
+tests/data/hls_start_number.m3u8: TAG = GEN
+tests/data/hls_start_number.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 3 -hls_list_size 0 -start_number 5 \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_start_number_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_start_number.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-start-number
+fate-hls-start-number: tests/data/hls_start_number.m3u8
+fate-hls-start-number: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_start_number.m3u8 -vf setpts=N*23
+
+# temp_file flag: exercises use_temp_file path in hls_start() / hls_write_trailer(),
+# writing to a .tmp file first and then renaming via hls_rename_temp_file().
+tests/data/hls_temp_file.m3u8: TAG = GEN
+tests/data/hls_temp_file.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=10" -f hls \
+ -hls_time 4 -hls_list_size 0 -hls_flags temp_file \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_temp_file_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_temp_file.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-temp-file
+fate-hls-temp-file: tests/data/hls_temp_file.m3u8
+fate-hls-temp-file: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_temp_file.m3u8 -vf setpts=N*23
+
+# baseurl option: exercises hls->baseurl prepending in hls_window() via
+# ff_hls_write_file_entry(hls->baseurl, ...).
+tests/data/hls_baseurl.m3u8: TAG = GEN
+tests/data/hls_baseurl.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=9" -f hls \
+ -hls_time 3 -hls_list_size 0 \
+ -hls_base_url "http://example.com/segments/" \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_baseurl_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_baseurl.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-baseurl
+fate-hls-baseurl: tests/data/hls_baseurl.m3u8
+fate-hls-baseurl: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_baseurl.m3u8 -vf setpts=N*23
+
+# allowcache=0: exercises hls->allowcache == 0 branch where
+# ff_hls_write_playlist_header writes #EXT-X-ALLOW-CACHE:NO.
+tests/data/hls_allow_cache.m3u8: TAG = GEN
+tests/data/hls_allow_cache.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+ $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+ -f lavfi -i "aevalsrc=cos(2*PI*t)*sin(2*PI*(440+4*t)*t):d=9" -f hls \
+ -hls_time 3 -hls_list_size 0 -hls_allow_cache 0 \
+ -codec:a mp2fixed \
+ -hls_segment_filename "$(TARGET_PATH)/tests/data/hls_allow_cache_%d.ts" \
+ $(TARGET_PATH)/tests/data/hls_allow_cache.m3u8 2>/dev/null
+
+FATE_HLSENC_EXTRA-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV) += fate-hls-allow-cache
+fate-hls-allow-cache: tests/data/hls_allow_cache.m3u8
+fate-hls-allow-cache: CMD = framecrc -auto_conversion_filters -flags +bitexact \
+ -i $(TARGET_PATH)/tests/data/hls_allow_cache.m3u8 -vf setpts=N*23
+
+FATE_SAMPLES_FFMPEG += $(FATE_HLSENC_EXTRA-yes)
+fate-hlsenc: $(FATE_HLSENC_EXTRA-yes)
diff --git a/tests/fate/libavcodec.mak b/tests/fate/libavcodec.mak
index e2d616e307..3d9b2e44b8 100644
--- a/tests/fate/libavcodec.mak
+++ b/tests/fate/libavcodec.mak
@@ -114,5 +114,10 @@ FATE_LIBAVCODEC-yes += fate-libavcodec-htmlsubtitles
fate-libavcodec-htmlsubtitles: libavcodec/tests/htmlsubtitles$(EXESUF)
fate-libavcodec-htmlsubtitles: CMD = run libavcodec/tests/htmlsubtitles$(EXESUF)
+FATE_LIBAVCODEC-$(CONFIG_CBS_H264) += fate-libavcodec-cbs-sei
+fate-libavcodec-cbs-sei: libavcodec/tests/cbs_sei$(EXESUF)
+fate-libavcodec-cbs-sei: CMD = run libavcodec/tests/cbs_sei$(EXESUF)
+fate-libavcodec-cbs-sei: CMP = null
+
FATE-$(CONFIG_AVCODEC) += $(FATE_LIBAVCODEC-yes)
fate-libavcodec: $(FATE_LIBAVCODEC-yes)
diff --git a/tests/fate/libavutil.mak b/tests/fate/libavutil.mak
index 6bf03b2438..1ac8f008c6 100644
--- a/tests/fate/libavutil.mak
+++ b/tests/fate/libavutil.mak
@@ -179,6 +179,11 @@ fate-uuid: libavutil/tests/uuid$(EXESUF)
fate-uuid: CMD = run libavutil/tests/uuid$(EXESUF)
fate-uuid: CMP = null
+FATE_LIBAVUTIL += fate-timecode
+fate-timecode: libavutil/tests/timecode$(EXESUF)
+fate-timecode: CMD = run libavutil/tests/timecode$(EXESUF)
+fate-timecode: CMP = null
+
FATE_LIBAVUTIL += $(FATE_LIBAVUTIL-yes)
FATE-$(CONFIG_AVUTIL) += $(FATE_LIBAVUTIL)
fate-libavutil: $(FATE_LIBAVUTIL)
--
2.52.0
_______________________________________________
ffmpeg-devel mailing list -- ffmpeg-devel@ffmpeg.org
To unsubscribe send an email to ffmpeg-devel-leave@ffmpeg.org
reply other threads:[~2026-02-25 14:45 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=177203066956.25.4466026026922548274@29965ddac10e \
--to=ffmpeg-devel@ffmpeg.org \
--cc=code@ffmpeg.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
This inbox may be cloned and mirrored by anyone:
git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git
# If you have public-inbox 1.1+ installed, you may
# initialize and index your mirror using the following commands:
public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
ffmpegdev@gitmailbox.com
public-inbox-index ffmpegdev
Example config snippet for mirrors.
AGPL code for this site: git clone https://public-inbox.org/public-inbox.git