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]
