PR #24829 opened by Jun Zhao (mypopydev)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24829
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24829.patch

This change resolves Ticket #24637 where hevc_mp4toannexb unconditionally 
prepended extradata VPS/SPS/PPS before every IRAP keyframe, causing redundant 
parameter sets when identical ones were already present in-band. It implements 
payload-exact deduplication using a hash table to suppress an extradata 
parameter set only when its NAL type, size, and byte contents match an in-band 
set preceding the IRAP, while safely preserving differing parameter sets (e.g., 
retaining extradata PPS 0 when only in-band PPS 1 is present) and declarative 
SEIs to prevent decoding failures. To guard against DoS risks, extradata is 
capped at 2,048 NALs to prevent memory amplification and in-band probe checks 
are bounded to prevent CPU amplification, with verification provided by an 
updated FATE conversion reference and a comprehensive 7-case API regression 
test suite (fate-api-hevc-mp4toannexb).


>From b4a8535ef71a01852c685f04ae272022247afd3a Mon Sep 17 00:00:00 2001
From: Jun Zhao <[email protected]>
Date: Thu, 1 Oct 2026 06:03:01 +0800
Subject: [PATCH 1/2] avcodec/hevc_mp4toannexb: avoid duplicating parameter
 sets when present in-band

When an input packet already contains in-band VPS/SPS/PPS parameter sets,
hevc_mp4toannexb still prepended extradata parameter sets before IRAP NALUs,
causing duplicate parameter sets at keyframes.

Avoid duplication by indexing extradata parameter sets in a hash table and
checking in-band parameter sets against it, suppressing any extradata
parameter set whose payload is already present in-band. Declarative SEIs
from extradata are always preserved. Packet-side matching attempts are
bounded to prevent CPU amplification.

Extradata is parsed in two passes with a single allocation, capping the
total number of NAL units to prevent memory amplification from malformed inputs.

Update the fate-hevc-bsf-mp4toannexb test reference accordingly.

Fixes: ticket #24637
Signed-off-by: Jun Zhao <[email protected]>
---
 libavcodec/bsf/hevc_mp4toannexb.c | 309 +++++++++++++++++++++++++-----
 tests/fate/hevc.mak               |   2 +-
 2 files changed, 263 insertions(+), 48 deletions(-)

diff --git a/libavcodec/bsf/hevc_mp4toannexb.c 
b/libavcodec/bsf/hevc_mp4toannexb.c
index 79633499b4..57100fdd91 100644
--- a/libavcodec/bsf/hevc_mp4toannexb.c
+++ b/libavcodec/bsf/hevc_mp4toannexb.c
@@ -21,6 +21,7 @@
 
 #include <string.h>
 
+#include "libavutil/common.h"
 #include "libavutil/intreadwrite.h"
 #include "libavutil/mem.h"
 
@@ -32,35 +33,89 @@
 #include "libavcodec/hevc/hevc.h"
 
 #define MIN_HEVCC_LENGTH 23
+#define MAX_EXTRADATA_NALS 2048
+#define MAX_PS_CHAIN_DEPTH 16
+#define MAX_PS_CHECKS_PER_PACKET 128
+
+/* 32-bit FNV-1a hash algorithm constants */
+#define FNV1A_32_OFFSET_BASIS 2166136261u
+#define FNV1A_32_PRIME        16777619u
+
+static inline uint32_t hash_ps(const uint8_t *data, size_t size)
+{
+    uint32_t h = FNV1A_32_OFFSET_BASIS;
+    for (size_t i = 0; i < size; i++) {
+        h ^= data[i];
+        h *= FNV1A_32_PRIME;
+    }
+    return h;
+}
+
+typedef struct HEVCNALUnit {
+    size_t   offset;
+    size_t   size;
+    int      type;
+    uint32_t hash;
+    int      matched;
+    int      emit;
+    int      next;
+} HEVCNALUnit;
 
 typedef struct HEVCBSFContext {
-    uint8_t *extradata;
-    size_t   extradata_size;
-    uint8_t  length_size;
-    int      extradata_parsed;
+    uint8_t     *extradata;
+    size_t       extradata_size;
+    HEVCNALUnit *nals;
+    int          nb_nals;
+    int          nb_ps;
+    int         *ps_buckets;
+    int          ps_table_size;
+    uint8_t      length_size;
+    int          extradata_parsed;
+    int          dup_ps_logged;
 } HEVCBSFContext;
 
 static int hevc_extradata_to_annexb(AVBSFContext *ctx,
-                                    const uint8_t *extradata, int 
extradata_size,
+                                    const uint8_t *extradata, size_t 
extradata_size,
                                     uint8_t **out_extradata, int 
*out_extradata_size)
 {
     HEVCBSFContext *s = ctx->priv_data;
-    GetByteContext gb;
+    GetByteContext gb, gb_scan;
     int length_size, num_arrays, i, j;
+    int total_nals = 0;
+    size_t total_size = 0;
     int ret = 0;
 
-    uint8_t *new_extradata = NULL;
-    size_t   new_extradata_size = 0;
+    uint8_t     *new_extradata = NULL;
+    size_t       new_extradata_size = 0;
+    HEVCNALUnit *new_nals = NULL;
+    int          nb_nals = 0;
+    int         *ps_buckets = NULL;
+    int          ps_table_size = 0;
+    int          nb_ps = 0;
 
-    bytestream2_init(&gb, extradata, extradata_size);
+    /* bytestream2 and AVCodecParameters.extradata_size are both int. */
+    if (extradata_size > INT_MAX) {
+        av_log(ctx, AV_LOG_ERROR, "Extradata size exceeds INT_MAX\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    bytestream2_init(&gb, extradata, (int)extradata_size);
 
     bytestream2_skip(&gb, 21);
     length_size = (bytestream2_get_byte(&gb) & 3) + 1;
     num_arrays  = bytestream2_get_byte(&gb);
 
+    /* First pass: validate hvcC arrays and compute exact size and NAL count 
upfront */
+    gb_scan = gb;
     for (i = 0; i < num_arrays; i++) {
-        int type = bytestream2_get_byte(&gb) & 0x3f;
-        int cnt  = bytestream2_get_be16(&gb);
+        int type, cnt;
+
+        if (bytestream2_get_bytes_left(&gb_scan) < 3) {
+            ret = AVERROR_INVALIDDATA;
+            goto fail;
+        }
+        type = bytestream2_get_byte(&gb_scan) & 0x3f;
+        cnt  = bytestream2_get_be16(&gb_scan);
 
         if (!(type == HEVC_NAL_VPS || type == HEVC_NAL_SPS || type == 
HEVC_NAL_PPS ||
               type == HEVC_NAL_SEI_PREFIX || type == HEVC_NAL_SEI_SUFFIX)) {
@@ -70,30 +125,88 @@ static int hevc_extradata_to_annexb(AVBSFContext *ctx,
             goto fail;
         }
 
-        for (j = 0; j < cnt; j++) {
-            const int nalu_len = bytestream2_get_be16(&gb);
+        if (cnt > MAX_EXTRADATA_NALS - total_nals) {
+            av_log(ctx, AV_LOG_ERROR, "Too many NAL units in extradata: 
exceeds %d\n",
+                   MAX_EXTRADATA_NALS);
+            ret = AVERROR_INVALIDDATA;
+            goto fail;
+        }
 
-            if (!nalu_len ||
-                nalu_len > bytestream2_get_bytes_left(&gb) ||
-                4 + AV_INPUT_BUFFER_PADDING_SIZE + nalu_len > SIZE_MAX - 
new_extradata_size) {
+        for (j = 0; j < cnt; j++) {
+            int nalu_len;
+
+            if (bytestream2_get_bytes_left(&gb_scan) < 2) {
                 ret = AVERROR_INVALIDDATA;
                 goto fail;
             }
-            ret = av_reallocp(&new_extradata, new_extradata_size + nalu_len + 
4 + AV_INPUT_BUFFER_PADDING_SIZE);
-            if (ret < 0)
+            nalu_len = bytestream2_get_be16(&gb_scan);
+
+            if (nalu_len < 2 ||
+                nalu_len > bytestream2_get_bytes_left(&gb_scan) ||
+                4 + AV_INPUT_BUFFER_PADDING_SIZE + nalu_len > SIZE_MAX - 
total_size) {
+                ret = AVERROR_INVALIDDATA;
                 goto fail;
+            }
+            /* Each hvcC length prefix grows into a 4-byte start code. */
+            if (total_size > (size_t)INT_MAX - 4 - (size_t)nalu_len) {
+                av_log(ctx, AV_LOG_ERROR, "Annex B extradata size exceeds 
INT_MAX\n");
+                ret = AVERROR_INVALIDDATA;
+                goto fail;
+            }
+            total_size += 4 + nalu_len;
+            total_nals++;
+            bytestream2_skip(&gb_scan, nalu_len);
+        }
+    }
+
+    /* Single allocation for exact extradata buffer and NAL descriptor array */
+    if (total_nals > 0) {
+        new_extradata = av_malloc(total_size + AV_INPUT_BUFFER_PADDING_SIZE);
+        if (!new_extradata) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+        new_nals = av_malloc_array(total_nals, sizeof(*new_nals));
+        if (!new_nals) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+    }
+
+    /* Second pass: copy NAL units and fill descriptor array without 
reallocations */
+    for (i = 0; i < num_arrays; i++) {
+        int type = bytestream2_get_byte(&gb) & 0x3f;
+        int cnt  = bytestream2_get_be16(&gb);
+
+        for (j = 0; j < cnt; j++) {
+            const int nalu_len = bytestream2_get_be16(&gb);
+
+            new_nals[nb_nals].offset  = new_extradata_size;
+            new_nals[nb_nals].size    = 4 + nalu_len;
+            new_nals[nb_nals].type    = type;
+            new_nals[nb_nals].matched = 0;
+            new_nals[nb_nals].emit    = 1;
+            new_nals[nb_nals].hash    = 0;
+            new_nals[nb_nals].next    = -1;
+            nb_nals++;
 
             AV_WB32(new_extradata + new_extradata_size, 1); // add the 
startcode
             bytestream2_get_buffer(&gb, new_extradata + new_extradata_size + 
4, nalu_len);
             new_extradata_size += 4 + nalu_len;
-            memset(new_extradata + new_extradata_size, 0, 
AV_INPUT_BUFFER_PADDING_SIZE);
         }
     }
+    if (new_extradata)
+        memset(new_extradata + new_extradata_size, 0, 
AV_INPUT_BUFFER_PADDING_SIZE);
 
     if (!new_extradata_size)
         av_log(ctx, AV_LOG_WARNING, "No parameter sets in the extradata\n");
 
     if (out_extradata && out_extradata_size) {
+        if (new_extradata_size > INT_MAX) {
+            av_log(ctx, AV_LOG_ERROR, "Annex B extradata size exceeds 
INT_MAX\n");
+            ret = AVERROR_INVALIDDATA;
+            goto fail;
+        }
         av_freep(out_extradata);
         *out_extradata =
             av_malloc(new_extradata_size + AV_INPUT_BUFFER_PADDING_SIZE);
@@ -101,21 +214,56 @@ static int hevc_extradata_to_annexb(AVBSFContext *ctx,
             ret = AVERROR(ENOMEM);
             goto fail;
         }
-        *out_extradata_size = new_extradata_size;
+        *out_extradata_size = (int)new_extradata_size;
         memcpy(*out_extradata, new_extradata, new_extradata_size);
         memset(*out_extradata + new_extradata_size, 0,
                AV_INPUT_BUFFER_PADDING_SIZE);
     }
 
-    av_freep(&s->extradata);
-    s->extradata = new_extradata;
-    s->extradata_size = new_extradata_size;
+    for (i = 0; i < nb_nals; i++) {
+        if (new_nals[i].type >= HEVC_NAL_VPS && new_nals[i].type <= 
HEVC_NAL_PPS)
+            nb_ps++;
+    }
 
-    s->length_size = length_size;
+    if (nb_ps > 0) {
+        ps_table_size = 1 << av_ceil_log2(FFMAX(32, nb_ps * 2));
+        ps_buckets = av_malloc_array(ps_table_size, sizeof(*ps_buckets));
+        if (!ps_buckets) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+        for (i = 0; i < ps_table_size; i++)
+            ps_buckets[i] = -1;
+
+        for (i = 0; i < nb_nals; i++) {
+            HEVCNALUnit *nal = &new_nals[i];
+            if (nal->type >= HEVC_NAL_VPS && nal->type <= HEVC_NAL_PPS) {
+                nal->hash = hash_ps(new_extradata + nal->offset + 4, nal->size 
- 4);
+                int bucket = nal->hash & (ps_table_size - 1);
+                nal->next = ps_buckets[bucket];
+                ps_buckets[bucket] = i;
+            }
+        }
+    }
+
+    av_freep(&s->nals);
+    av_freep(&s->extradata);
+    av_freep(&s->ps_buckets);
+    s->extradata        = new_extradata;
+    s->extradata_size   = new_extradata_size;
+    s->nals             = new_nals;
+    s->nb_nals          = nb_nals;
+    s->nb_ps            = nb_ps;
+    s->ps_buckets       = ps_buckets;
+    s->ps_table_size    = ps_table_size;
+    s->length_size      = length_size;
     s->extradata_parsed = 1;
+    s->dup_ps_logged    = 0;
     return 0;
 fail:
     av_freep(&new_extradata);
+    av_freep(&new_nals);
+    av_freep(&ps_buckets);
     return ret;
 }
 
@@ -151,6 +299,7 @@ static int hevc_mp4toannexb_filter(AVBSFContext *ctx, 
AVPacket *out)
     int got_ps = 0, seen_irap_ps = 0;
     int i, ret = 0;
     size_t extradata_size = 0;
+    size_t extra_insert_size = 0;
     uint8_t *extradata = NULL;
 
     ret = ff_bsf_get_packet(ctx, &in);
@@ -178,37 +327,94 @@ static int hevc_mp4toannexb_filter(AVBSFContext *ctx, 
AVPacket *out)
 
     bytestream2_init(&gb, in->data, in->size);
 
-    while (!got_irap && bytestream2_get_bytes_left(&gb)) {
-        uint32_t nalu_size = 0;
-        int      nalu_type;
+    for (i = 0; i < s->nb_nals; i++)
+        s->nals[i].matched = 0;
 
-        if (bytestream2_get_bytes_left(&gb) < s->length_size) {
-            ret = AVERROR_INVALIDDATA;
-            goto fail;
+    {
+        int unmatched_ps = s->nb_ps;
+        int ps_checks = 0;
+
+        while (!got_irap && bytestream2_get_bytes_left(&gb)) {
+            uint32_t nalu_size = 0;
+            int      nalu_type;
+
+            if (bytestream2_get_bytes_left(&gb) < s->length_size) {
+                ret = AVERROR_INVALIDDATA;
+                goto fail;
+            }
+            for (i = 0; i < s->length_size; i++)
+                nalu_size = (nalu_size << 8) | bytestream2_get_byte(&gb);
+
+            if (nalu_size < 2 || nalu_size > bytestream2_get_bytes_left(&gb)) {
+                ret = AVERROR_INVALIDDATA;
+                goto fail;
+            }
+
+            nalu_type = (bytestream2_peek_byte(&gb) >> 1) & 0x3f;
+            if (nalu_type >= HEVC_NAL_VPS && nalu_type <= HEVC_NAL_PPS) {
+                if (unmatched_ps > 0 && s->ps_buckets &&
+                    ps_checks < MAX_PS_CHECKS_PER_PACKET) {
+                    uint32_t hash;
+                    int idx, depth = 0;
+
+                    ps_checks++;
+                    hash = hash_ps(gb.buffer, nalu_size);
+                    idx  = s->ps_buckets[hash & (s->ps_table_size - 1)];
+
+                    while (idx >= 0 && depth++ < MAX_PS_CHAIN_DEPTH) {
+                        HEVCNALUnit *nal = &s->nals[idx];
+                        if (nal->type == nalu_type && (nal->size - 4) == 
nalu_size &&
+                            nal->hash == hash &&
+                            !memcmp(s->extradata + nal->offset + 4, gb.buffer, 
nalu_size)) {
+                            if (!nal->matched) {
+                                nal->matched = 1;
+                                unmatched_ps--;
+                            }
+                        }
+                        idx = nal->next;
+                    }
+                }
+            }
+            bytestream2_skip(&gb, nalu_size);
+            got_irap |= nalu_type >= HEVC_NAL_BLA_W_LP &&
+                        nalu_type <= HEVC_NAL_RSV_IRAP_VCL23;
+            got_ps   |= nalu_type >= HEVC_NAL_VPS && nalu_type <= HEVC_NAL_PPS;
         }
-        for (i = 0; i < s->length_size; i++)
-            nalu_size = (nalu_size << 8) | bytestream2_get_byte(&gb);
-
-        if (nalu_size < 2 || nalu_size > bytestream2_get_bytes_left(&gb)) {
-            ret = AVERROR_INVALIDDATA;
-            goto fail;
-        }
-
-        nalu_type = (bytestream2_peek_byte(&gb) >> 1) & 0x3f;
-        bytestream2_skip(&gb, nalu_size);
-        got_irap |= nalu_type >= HEVC_NAL_BLA_W_LP &&
-                    nalu_type <= HEVC_NAL_RSV_IRAP_VCL23;
-        got_ps   |= nalu_type >= HEVC_NAL_VPS && nalu_type <= HEVC_NAL_PPS;
     }
     seen_irap_ps = got_irap && got_ps;
     got_irap = got_ps = 0;
 
+    {
+        int omitted = 0;
+
+        for (i = 0; i < s->nb_nals; i++) {
+            HEVCNALUnit *nal = &s->nals[i];
+
+            nal->emit = 1;
+            if (seen_irap_ps && nal->matched &&
+                nal->type >= HEVC_NAL_VPS && nal->type <= HEVC_NAL_PPS) {
+                nal->emit = 0;
+                omitted++;
+            }
+
+            if (nal->emit)
+                extra_insert_size += nal->size;
+        }
+        if (omitted && !s->dup_ps_logged) {
+            av_log(ctx, AV_LOG_VERBOSE,
+                   "Omitting %d extradata parameter sets already present 
in-band\n",
+                   omitted);
+            s->dup_ps_logged = 1;
+        }
+    }
+
     bytestream2_init(&gb, in->data, in->size);
 
     while (bytestream2_get_bytes_left(&gb)) {
         uint32_t nalu_size = 0;
         int      nalu_type;
-        int is_irap, is_ps, add_extradata, extra_size, prev_size;
+        int is_irap, is_ps, add_extradata, prev_size;
+        size_t extra_size;
 
         if (bytestream2_get_bytes_left(&gb) < s->length_size) {
             ret = AVERROR_INVALIDDATA;
@@ -229,7 +435,7 @@ static int hevc_mp4toannexb_filter(AVBSFContext *ctx, 
AVPacket *out)
                   nalu_type <= HEVC_NAL_RSV_IRAP_VCL23;
         is_ps   = nalu_type >= HEVC_NAL_VPS && nalu_type <= HEVC_NAL_PPS && 
seen_irap_ps;
         add_extradata = (is_ps || is_irap) && !got_ps && !got_irap;
-        extra_size    = add_extradata * s->extradata_size;
+        extra_size    = add_extradata ? extra_insert_size : 0;
         got_irap     |= is_irap;
         got_ps       |= is_ps;
 
@@ -244,8 +450,15 @@ static int hevc_mp4toannexb_filter(AVBSFContext *ctx, 
AVPacket *out)
         if (ret < 0)
             goto fail;
 
-        if (extra_size)
-            memcpy(out->data + prev_size, s->extradata, extra_size);
+        if (extra_size) {
+            uint8_t *dst = out->data + prev_size;
+            for (i = 0; i < s->nb_nals; i++) {
+                if (s->nals[i].emit) {
+                    memcpy(dst, s->extradata + s->nals[i].offset, 
s->nals[i].size);
+                    dst += s->nals[i].size;
+                }
+            }
+        }
         AV_WB32(out->data + prev_size + extra_size, 1);
         bytestream2_get_buffer(&gb, out->data + prev_size + 4 + extra_size, 
nalu_size);
     }
@@ -265,7 +478,9 @@ fail:
 static void hevc_mp4toannexb_close(AVBSFContext *ctx)
 {
     HEVCBSFContext *s = ctx->priv_data;
+    av_freep(&s->nals);
     av_freep(&s->extradata);
+    av_freep(&s->ps_buckets);
 }
 
 static const enum AVCodecID codec_ids[] = {
diff --git a/tests/fate/hevc.mak b/tests/fate/hevc.mak
index 10b6cd59ae..ba5242e0d2 100644
--- a/tests/fate/hevc.mak
+++ b/tests/fate/hevc.mak
@@ -233,7 +233,7 @@ FATE_HEVC-$(call DEMMUX, HEVC MOV, MOV HEVC, HEVC_PARSER 
HEVC_MP4TOANNEXB_BSF EX
 fate-hevc-bsf-mp4toannexb: tests/data/hevc-mp4.mov
 fate-hevc-bsf-mp4toannexb: CMD = md5 -i $(TARGET_PATH)/tests/data/hevc-mp4.mov 
-c:v copy -fflags +bitexact -f hevc
 fate-hevc-bsf-mp4toannexb: CMP = oneline
-fate-hevc-bsf-mp4toannexb: REF = 73019329ed7f81c24f9af67c34c640c0
+fate-hevc-bsf-mp4toannexb: REF = 7d05a79c7a6665ae22c0043a4d83a811
 
 FATE_HEVC-$(call DEMMUX, HEVC MOV, MOV HEVC, HEVC_PARSER HEVC_MP4TOANNEXB_BSF 
EXTRACT_EXTRADATA_BSF HEVC_METADATA_BSF SCALE_FILTER) += 
fate-hevc-bsf-mp4toannexb-new-extradata
 fate-hevc-bsf-mp4toannexb-new-extradata: CMD = stream_remux mov 
$(TARGET_SAMPLES)/hevc/extradata-reload-multi-stsd.mov "" hevc "-bsf:v 
hevc_mp4toannexb,hevc_metadata -map 0:v"
-- 
2.52.0


>From 9697b403bc1d2856b9f2f82dfff3de7b102c50af Mon Sep 17 00:00:00 2001
From: Jun Zhao <[email protected]>
Date: Thu, 1 Oct 2026 06:03:12 +0800
Subject: [PATCH 2/2] tests/fate/api: add api-hevc-mp4toannexb test

Add an API test for hevc_mp4toannexb parameter set deduplication,
covering extradata insertion, exact payload deduplication, retaining
differing parameter sets (PPS 0 vs PPS 1), SEI preservation,
many in-band parameter sets before IRAP, duplicate extradata
parameter set deduplication, and exceeding MAX_EXTRADATA_NALS limit
rejection.

Signed-off-by: Jun Zhao <[email protected]>
---
 tests/api/Makefile                    |   1 +
 tests/api/api-hevc-mp4toannexb-test.c | 379 ++++++++++++++++++++++++++
 tests/fate/api.mak                    |   4 +
 tests/ref/fate/api-hevc-mp4toannexb   |  14 +
 4 files changed, 398 insertions(+)
 create mode 100644 tests/api/api-hevc-mp4toannexb-test.c
 create mode 100644 tests/ref/fate/api-hevc-mp4toannexb

diff --git a/tests/api/Makefile b/tests/api/Makefile
index f2738d4cba..280620a6ab 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -4,6 +4,7 @@ APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264
 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264-slice
 APITESTPROGS-$(CONFIG_MOV_MUXER)        += api-movenc
 APITESTPROGS-$(CONFIG_MOVTEXT_ENCODER)  += api-movtext
+APITESTPROGS-$(CONFIG_HEVC_MP4TOANNEXB_BSF) += api-hevc-mp4toannexb
 APITESTPROGS-yes += api-seek api-dump-stream-meta
 APITESTPROGS-$(call DEMDEC, H263, H263) += api-band
 APITESTPROGS-$(HAVE_THREADS) += api-threadmessage
diff --git a/tests/api/api-hevc-mp4toannexb-test.c 
b/tests/api/api-hevc-mp4toannexb-test.c
new file mode 100644
index 0000000000..67db4a38bc
--- /dev/null
+++ b/tests/api/api-hevc-mp4toannexb-test.c
@@ -0,0 +1,379 @@
+/*
+ * Test for HEVC mp4toannexb bitstream filter parameter set deduplication
+ *
+ * Copyright (c) 2026 FFmpeg developers
+ *
+ * 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
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/error.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/macros.h"
+#include "libavutil/mem.h"
+#include "libavcodec/bsf.h"
+#include "libavcodec/packet.h"
+
+static const uint8_t vps_bytes[] = {
+    0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00,
+    0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x94, 0x90, 
0x24
+};
+
+static const uint8_t sps_bytes[] = {
+    0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
+    0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3c, 0xa0, 0x0d, 0x08, 0x0f, 0x1f,
+    0xb6, 0x59, 0x49, 0x92, 0x46, 0xd8, 0x61, 0x62, 0x2a, 0xa4, 0xc4, 0xc3,
+    0x2f, 0xb3, 0xeb, 0xcd, 0xf9, 0x67, 0xd7, 0x85, 0x11, 0x89, 0xcb, 0xbc,
+    0x00, 0x0f, 0x88
+};
+
+/* PPS with pps_pic_parameter_set_id = 0 */
+static const uint8_t pps0_bytes[] = {
+    0x44, 0x01, 0xe1, 0xa5, 0x64, 0xc1, 0x8d
+};
+
+/* PPS with valid H.265 RBSP syntax for pps_pic_parameter_set_id = 1 */
+static const uint8_t pps1_bytes[] = {
+    0x44, 0x01, 0x58, 0x69, 0x59, 0x30, 0x63, 0x40
+};
+
+/* Prefix SEI (e.g. recovery point / declarative SEI) */
+static const uint8_t prefix_sei_bytes[] = {
+    0x4e, 0x01, 0x06, 0x01, 0xd0, 0x80
+};
+
+/* Valid 2-byte Prefix SEI header for cap-testing */
+static const uint8_t dummy_sei_bytes[] = {
+    0x4e, 0x01
+};
+
+/* IDR slice NAL unit */
+static const uint8_t idr_slice_bytes[] = {
+    0x26, 0x01, 0xaf, 0x34, 0x83, 0x1b, 0x8a, 0x33
+};
+
+static const uint8_t hvcc_header[22] = {
+    1,             /* configurationVersion = 1 */
+    0x01,          /* general_profile_space / tier / profile_idc */
+    0x60, 0, 0, 0, /* general_profile_compatibility_flags */
+    0x90, 0, 0, 0, 0, 0, /* general_constraint_indicator_flags */
+    0x5d,          /* general_level_idc */
+    0xf0, 0x00,    /* min_spatial_segmentation_idc (1111 0000) */
+    0xfc,          /* parallelismType */
+    0xfd,          /* chroma_format_idc (3 = 4:2:0) */
+    0xf8,          /* bit_depth_luma_minus8 */
+    0xf8,          /* bit_depth_chroma_minus8 */
+    0x00, 0x00,    /* avgFrameRate */
+    0x03           /* lengthSizeMinusOne = 3 (4-byte length prefix) */
+};
+
+typedef struct HVCCNALDesc {
+    int            type;
+    const uint8_t *data;
+    size_t         size;
+    int            count;
+} HVCCNALDesc;
+
+static void print_packet_hex(const char *label, const AVPacket *pkt)
+{
+    printf("%s: size %d, hex: ", label, pkt->size);
+    for (int i = 0; i < pkt->size; i++)
+        printf("%02x", pkt->data[i]);
+    printf("\n");
+}
+
+/* Helper to build a HEVCDecoderConfigurationRecord (hvcC) from array 
descriptors */
+static uint8_t *build_hvcc(size_t *hvcc_size, const HVCCNALDesc *arrays, int 
nb_arrays)
+{
+    size_t total = sizeof(hvcc_header) + 1;
+    uint8_t *buf, *p;
+
+    for (int i = 0; i < nb_arrays; i++)
+        total += 3 + (size_t)arrays[i].count * (2 + arrays[i].size);
+
+    buf = av_mallocz(total + AV_INPUT_BUFFER_PADDING_SIZE);
+    if (!buf)
+        return NULL;
+
+    p = buf;
+    memcpy(p, hvcc_header, sizeof(hvcc_header));
+    p += sizeof(hvcc_header);
+    *p++ = nb_arrays;
+
+    for (int i = 0; i < nb_arrays; i++) {
+        int cnt = arrays[i].count;
+        size_t len = arrays[i].size;
+
+        *p++ = arrays[i].type & 0x3f;
+        *p++ = (cnt >> 8) & 0xff;
+        *p++ = cnt & 0xff;
+        for (int j = 0; j < cnt; j++) {
+            *p++ = (len >> 8) & 0xff;
+            *p++ = len & 0xff;
+            if (arrays[i].data)
+                memcpy(p, arrays[i].data, len);
+            else
+                memset(p, 0x80, len);
+            p += len;
+        }
+    }
+
+    *hvcc_size = p - buf;
+    return buf;
+}
+
+/* Helper to build a packet with 4-byte length-prefixed NALUs */
+static int build_packet(AVPacket *pkt,
+                        const uint8_t *nalus[], const size_t lens[], int count)
+{
+    size_t total = 0;
+    uint8_t *p;
+    int ret;
+
+    for (int i = 0; i < count; i++)
+        total += 4 + lens[i];
+
+    ret = av_new_packet(pkt, total);
+    if (ret < 0)
+        return ret;
+
+    p = pkt->data;
+    for (int i = 0; i < count; i++) {
+        AV_WB32(p, lens[i]);
+        memcpy(p + 4, nalus[i], lens[i]);
+        p += 4 + lens[i];
+    }
+
+    return 0;
+}
+
+static int run_test_case(const char *case_name,
+                         const uint8_t *hvcc, size_t hvcc_size,
+                         AVPacket *in_pkt, int expected_init_ret)
+{
+    const AVBitStreamFilter *bsf;
+    AVBSFContext *ctx = NULL;
+    AVPacket *out_pkt = NULL;
+    int ret = 0;
+
+    printf("=== Test: %s ===\n", case_name);
+
+    bsf = av_bsf_get_by_name("hevc_mp4toannexb");
+    if (!bsf) {
+        fprintf(stderr, "Bitstream filter hevc_mp4toannexb not found\n");
+        return -1;
+    }
+
+    ret = av_bsf_alloc(bsf, &ctx);
+    if (ret < 0)
+        return ret;
+
+    ctx->par_in->codec_type = AVMEDIA_TYPE_VIDEO;
+    ctx->par_in->codec_id   = AV_CODEC_ID_HEVC;
+    ctx->par_in->extradata  = av_memdup(hvcc, hvcc_size);
+    if (!ctx->par_in->extradata) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    ctx->par_in->extradata_size = hvcc_size;
+
+    ret = av_bsf_init(ctx);
+    if (expected_init_ret < 0) {
+        if (ret == expected_init_ret) {
+            printf("av_bsf_init correctly failed with AVERROR_INVALIDDATA\n");
+            ret = 0;
+        } else {
+            fprintf(stderr, "av_bsf_init returned %d, expected %d\n",
+                    ret, expected_init_ret);
+            ret = -1;
+        }
+        goto end;
+    } else if (ret < 0) {
+        char errbuf[AV_ERROR_MAX_STRING_SIZE];
+        av_strerror(ret, errbuf, sizeof(errbuf));
+        fprintf(stderr, "av_bsf_init failed: %s\n", errbuf);
+        goto end;
+    }
+
+    out_pkt = av_packet_alloc();
+    if (!out_pkt) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+
+    ret = av_bsf_send_packet(ctx, in_pkt);
+    if (ret < 0) {
+        fprintf(stderr, "av_bsf_send_packet failed: %d\n", ret);
+        goto end;
+    }
+
+    ret = av_bsf_receive_packet(ctx, out_pkt);
+    if (ret < 0) {
+        fprintf(stderr, "av_bsf_receive_packet failed: %d\n", ret);
+        goto end;
+    }
+
+    print_packet_hex("Output", out_pkt);
+    ret = 0;
+
+end:
+    av_packet_free(&out_pkt);
+    av_bsf_free(&ctx);
+    return ret;
+}
+
+int main(void)
+{
+    uint8_t *hvcc_std = NULL, *hvcc_sei = NULL, *hvcc_dup = NULL, *hvcc_limit 
= NULL;
+    size_t sz_std = 0, sz_sei = 0, sz_dup = 0, sz_limit = 0;
+    AVPacket *pkt = av_packet_alloc();
+    int ret = 0, err;
+
+    const HVCCNALDesc std_arrays[] = {
+        { 32, vps_bytes,  sizeof(vps_bytes),  1 },
+        { 33, sps_bytes,  sizeof(sps_bytes),  1 },
+        { 34, pps0_bytes, sizeof(pps0_bytes), 1 },
+    };
+    const HVCCNALDesc sei_arrays[] = {
+        { 39, prefix_sei_bytes, sizeof(prefix_sei_bytes), 1 },
+        { 32, vps_bytes,        sizeof(vps_bytes),        1 },
+        { 33, sps_bytes,        sizeof(sps_bytes),        1 },
+        { 34, pps0_bytes,       sizeof(pps0_bytes),       1 },
+    };
+    const HVCCNALDesc dup_arrays[] = {
+        { 32, vps_bytes,  sizeof(vps_bytes),  1 },
+        { 33, sps_bytes,  sizeof(sps_bytes),  2 },
+        { 34, pps0_bytes, sizeof(pps0_bytes), 1 },
+    };
+    const HVCCNALDesc limit_arrays[] = {
+        { 39, dummy_sei_bytes, sizeof(dummy_sei_bytes), 2049 },
+    };
+
+    if (!pkt)
+        return 1;
+
+    hvcc_std   = build_hvcc(&sz_std,   std_arrays,   
FF_ARRAY_ELEMS(std_arrays));
+    hvcc_sei   = build_hvcc(&sz_sei,   sei_arrays,   
FF_ARRAY_ELEMS(sei_arrays));
+    hvcc_dup   = build_hvcc(&sz_dup,   dup_arrays,   
FF_ARRAY_ELEMS(dup_arrays));
+    hvcc_limit = build_hvcc(&sz_limit, limit_arrays, 
FF_ARRAY_ELEMS(limit_arrays));
+
+    if (!hvcc_std || !hvcc_sei || !hvcc_dup || !hvcc_limit) {
+        ret = 1;
+        goto cleanup;
+    }
+
+    /* Case 1: Standard insertion (extradata has VPS, SPS, PPS 0; packet only 
has IDR slice) */
+    {
+        const uint8_t *nalus[] = { idr_slice_bytes };
+        const size_t lens[]    = { sizeof(idr_slice_bytes) };
+        build_packet(pkt, nalus, lens, FF_ARRAY_ELEMS(nalus));
+        err = run_test_case("Case 1 (Standard Insertion)", hvcc_std, sz_std, 
pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 2: Exact payload deduplication (extradata has VPS, SPS, PPS 0; 
packet has identical in-band VPS, SPS, PPS 0 + IDR slice) */
+    {
+        const uint8_t *nalus[] = { vps_bytes, sps_bytes, pps0_bytes, 
idr_slice_bytes };
+        const size_t lens[]    = { sizeof(vps_bytes), sizeof(sps_bytes), 
sizeof(pps0_bytes), sizeof(idr_slice_bytes) };
+        build_packet(pkt, nalus, lens, FF_ARRAY_ELEMS(nalus));
+        err = run_test_case("Case 2 (Exact Payload Deduplication)", hvcc_std, 
sz_std, pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 3: Reviewer Fairy counterexample (extradata has VPS, SPS, PPS 0; 
packet has in-band VPS, SPS, PPS 1 + IDR slice) */
+    {
+        const uint8_t *nalus[] = { vps_bytes, sps_bytes, pps1_bytes, 
idr_slice_bytes };
+        const size_t lens[]    = { sizeof(vps_bytes), sizeof(sps_bytes), 
sizeof(pps1_bytes), sizeof(idr_slice_bytes) };
+        build_packet(pkt, nalus, lens, FF_ARRAY_ELEMS(nalus));
+        err = run_test_case("Case 3 (Fairy Counterexample - Retain PPS 0)", 
hvcc_std, sz_std, pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 4: Declarative SEI preservation (extradata has Prefix SEI, VPS, 
SPS, PPS 0; packet has identical VPS, SPS, PPS 0 + IDR slice) */
+    {
+        const uint8_t *nalus[] = { vps_bytes, sps_bytes, pps0_bytes, 
idr_slice_bytes };
+        const size_t lens[]    = { sizeof(vps_bytes), sizeof(sps_bytes), 
sizeof(pps0_bytes), sizeof(idr_slice_bytes) };
+        build_packet(pkt, nalus, lens, FF_ARRAY_ELEMS(nalus));
+        err = run_test_case("Case 4 (Declarative SEI Preservation)", hvcc_sei, 
sz_sei, pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 5: Many in-band parameter sets before IRAP (threshold coverage) */
+    {
+        const uint8_t *nalus[36];
+        size_t lens[36];
+        int count = 0;
+
+        nalus[count] = vps_bytes;
+        lens[count++] = sizeof(vps_bytes);
+
+        nalus[count] = sps_bytes;
+        lens[count++] = sizeof(sps_bytes);
+
+        for (int k = 0; k < 32; k++) {
+            nalus[count] = pps1_bytes;
+            lens[count++] = sizeof(pps1_bytes);
+        }
+
+        nalus[count] = pps0_bytes;
+        lens[count++] = sizeof(pps0_bytes);
+
+        nalus[count] = idr_slice_bytes;
+        lens[count++] = sizeof(idr_slice_bytes);
+
+        build_packet(pkt, nalus, lens, count);
+        err = run_test_case("Case 5 (>32 In-band PS Threshold Coverage)", 
hvcc_std, sz_std, pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 6: Duplicate extradata entries deduplication (extradata has 1xVPS, 
2xSPS, 1xPPS 0; packet has 1xVPS, 1xSPS, 1xPPS 0 + IDR slice) */
+    {
+        const uint8_t *nalus[] = { vps_bytes, sps_bytes, pps0_bytes, 
idr_slice_bytes };
+        const size_t lens[]    = { sizeof(vps_bytes), sizeof(sps_bytes), 
sizeof(pps0_bytes), sizeof(idr_slice_bytes) };
+        build_packet(pkt, nalus, lens, FF_ARRAY_ELEMS(nalus));
+        err = run_test_case("Case 6 (Duplicate Extradata PS Deduplication)", 
hvcc_dup, sz_dup, pkt, 0);
+        if (err < 0)
+            ret = 1;
+        av_packet_unref(pkt);
+    }
+
+    /* Case 7: Exceeds MAX_EXTRADATA_NALS limit rejection (hvcC carries >2048 
NALUs) */
+    err = run_test_case("Case 7 (Exceeds MAX_EXTRADATA_NALS Limit Rejection)",
+                        hvcc_limit, sz_limit, NULL, AVERROR_INVALIDDATA);
+    if (err < 0)
+        ret = 1;
+
+cleanup:
+    av_freep(&hvcc_std);
+    av_freep(&hvcc_sei);
+    av_freep(&hvcc_dup);
+    av_freep(&hvcc_limit);
+    av_packet_free(&pkt);
+    return ret;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index a10edfe4da..20d82be9ce 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -11,6 +11,10 @@ FATE_API_LIBAVCODEC-$(CONFIG_MOVTEXT_ENCODER) += 
fate-api-movtext
 fate-api-movtext: $(APITESTSDIR)/api-movtext-test$(EXESUF)
 fate-api-movtext: CMD = run $(APITESTSDIR)/api-movtext-test$(EXESUF)
 
+FATE_API_LIBAVCODEC-$(CONFIG_HEVC_MP4TOANNEXB_BSF) += fate-api-hevc-mp4toannexb
+fate-api-hevc-mp4toannexb: $(APITESTSDIR)/api-hevc-mp4toannexb-test$(EXESUF)
+fate-api-hevc-mp4toannexb: CMD = run 
$(APITESTSDIR)/api-hevc-mp4toannexb-test$(EXESUF)
+
 FATE_API_LIBAVCODEC-$(call ALLYES, H261_ENCODER H261_PARSER) += 
fate-api-enc-parser fate-api-enc-parser-cif
 fate-api-enc-parser: $(APITESTSDIR)/api-enc-parser-test$(EXESUF)
 fate-api-enc-parser: CMD = run $(APITESTSDIR)/api-enc-parser-test$(EXESUF) 
h261 176 144
diff --git a/tests/ref/fate/api-hevc-mp4toannexb 
b/tests/ref/fate/api-hevc-mp4toannexb
new file mode 100644
index 0000000000..cd6b2198c7
--- /dev/null
+++ b/tests/ref/fate/api-hevc-mp4toannexb
@@ -0,0 +1,14 @@
+=== Test: Case 1 (Standard Insertion) ===
+Output: size 107, hex: 
0000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401e1a564c18d000000012601af34831b8a33
+=== Test: Case 2 (Exact Payload Deduplication) ===
+Output: size 107, hex: 
0000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401e1a564c18d000000012601af34831b8a33
+=== Test: Case 3 (Fairy Counterexample - Retain PPS 0) ===
+Output: size 119, hex: 
000000014401e1a564c18d0000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401586959306340000000012601af34831b8a33
+=== Test: Case 4 (Declarative SEI Preservation) ===
+Output: size 117, hex: 
000000014e010601d0800000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401e1a564c18d000000012601af34831b8a33
+=== Test: Case 5 (>32 In-band PS Threshold Coverage) ===
+Output: size 491, hex: 
0000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401586959306340000000014401e1a564c18d000000012601af34
 831b8a33
+=== Test: Case 6 (Duplicate Extradata PS Deduplication) ===
+Output: size 107, hex: 
0000000140010c01ffff0220000003000003000003000003003c949024000000014201010220000003000003000003000003003ca00d080f1fb659499246d861622aa4c4c32fb3ebcdf967d7851189cbbc000f88000000014401e1a564c18d000000012601af34831b8a33
+=== Test: Case 7 (Exceeds MAX_EXTRADATA_NALS Limit Rejection) ===
+av_bsf_init correctly failed with AVERROR_INVALIDDATA
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to