Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
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