PR #21301 opened by Jun Zhao (mypopydev) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21301 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21301.patch
This implementation enables full support for muxing and demuxing AV1 video within MPEG-2 Transport Streams, adhering to the AOMedia draft specification(https://aomediacodec.github.io/av1-mpeg2-ts/). A new bitstream filter, av1_ts, was introduced to seamlessly convert between the Low Overhead Bitstream Format (used in MP4/WebM) and the Start Code Based Format required for MPEG-TS, handling essential tasks such as start code insertion, emulation prevention, and Temporal Delimiter generation. The MPEG-TS muxer and demuxer have been updated to automatically integrate this filter and correctly process the specific AV1 Registration and Video Descriptors to ensure compliance and compatibility. From 53ff2304420553b12748f8d32f144fdc4aae72e4 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:42:18 +0800 Subject: [PATCH 01/11] lavc/bsf/av1_ts: add AV1 TS bitstream filter This bitstream filter converts AV1 streams between Annex B and Low Overhead bitstream format as defined in the "AV1 Codec in MPEG-2 TS" specification. MPEG-2 Transport Streams require AV1 to be in Annex B format (start codes), while FFmpeg's internal representation (and ISOBMFF) uses the Low Overhead format (length prefixed). This filter bridges the gap. Signed-off-by: Jun Zhao <[email protected]> --- configure | 1 + libavcodec/bitstream_filters.c | 1 + libavcodec/bsf/Makefile | 1 + libavcodec/bsf/av1_ts.c | 375 +++++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 libavcodec/bsf/av1_ts.c diff --git a/configure b/configure index 301a3e5e3e..02411d9327 100755 --- a/configure +++ b/configure @@ -3599,6 +3599,7 @@ ahx_to_mp2_bsf_deps="lgpl_gpl" av1_frame_merge_bsf_select="cbs_av1" av1_frame_split_bsf_select="cbs_av1" av1_metadata_bsf_select="cbs_av1" +av1_ts_bsf_select="av1_parse" dovi_rpu_bsf_select="cbs_h265 cbs_av1 dovi_rpudec dovi_rpuenc" dts2pts_bsf_select="cbs_h264 h264parse cbs_h265 hevc_parser" eac3_core_bsf_select="ac3_parser" diff --git a/libavcodec/bitstream_filters.c b/libavcodec/bitstream_filters.c index b4b852c7e6..49c7141de0 100644 --- a/libavcodec/bitstream_filters.c +++ b/libavcodec/bitstream_filters.c @@ -30,6 +30,7 @@ extern const FFBitStreamFilter ff_apv_metadata_bsf; extern const FFBitStreamFilter ff_av1_frame_merge_bsf; extern const FFBitStreamFilter ff_av1_frame_split_bsf; extern const FFBitStreamFilter ff_av1_metadata_bsf; +extern const FFBitStreamFilter ff_av1_ts_bsf; extern const FFBitStreamFilter ff_chomp_bsf; extern const FFBitStreamFilter ff_dump_extradata_bsf; extern const FFBitStreamFilter ff_dca_core_bsf; diff --git a/libavcodec/bsf/Makefile b/libavcodec/bsf/Makefile index 360fd1e32f..5cafb136c7 100644 --- a/libavcodec/bsf/Makefile +++ b/libavcodec/bsf/Makefile @@ -7,6 +7,7 @@ OBJS-$(CONFIG_APV_METADATA_BSF) += bsf/apv_metadata.o OBJS-$(CONFIG_AV1_FRAME_MERGE_BSF) += bsf/av1_frame_merge.o OBJS-$(CONFIG_AV1_FRAME_SPLIT_BSF) += bsf/av1_frame_split.o OBJS-$(CONFIG_AV1_METADATA_BSF) += bsf/av1_metadata.o +OBJS-$(CONFIG_AV1_TS_BSF) += bsf/av1_ts.o OBJS-$(CONFIG_CHOMP_BSF) += bsf/chomp.o OBJS-$(CONFIG_DCA_CORE_BSF) += bsf/dca_core.o OBJS-$(CONFIG_DTS2PTS_BSF) += bsf/dts2pts.o diff --git a/libavcodec/bsf/av1_ts.c b/libavcodec/bsf/av1_ts.c new file mode 100644 index 0000000000..5a7779c4a1 --- /dev/null +++ b/libavcodec/bsf/av1_ts.c @@ -0,0 +1,375 @@ +/* + * AV1 MPEG-TS bitstream filter + * Copyright (c) 2024 + * + * 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 + */ + +/** + * @file + * This bitstream filter converts AV1 bitstreams between Low Overhead Bitstream + * Format (used in MP4/WebM) and Start Code Based Format (used in MPEG-TS) as + * defined in "AV1 Codec in MPEG-2 Transport Stream" specification. + * + * Start Code Based Format prepends each OBU with a 3-byte start code (0x000001) + * and applies emulation prevention bytes (0x03) to prevent start code emulation. + */ + +#include "libavutil/avassert.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +#include "bsf.h" +#include "bsf_internal.h" +#include "av1.h" +#include "av1_parse.h" + +/* Start code for AV1 OBUs in MPEG-TS */ +#define AV1_START_CODE 0x000001 +#define START_CODE_SIZE 3 +#define MIN_PACKET_SIZE 4 +#define BUFFER_PADDING 64 + +/* Temporal Delimiter OBU: type=2, extension_flag=0, has_size_field=1, reserved=0 + * Binary: 0 0010 0 1 0 = 0x12, followed by size=0 (LEB128) */ +static const uint8_t temporal_delimiter_obu[] = { 0x12, 0x00 }; + +typedef struct AV1TSContext { + const AVClass *class; + int mode; /* 0 = to TS (add start codes), 1 = from TS (remove start codes) */ +} AV1TSContext; + +/** + * Prepare output packet by allocating buffer and copying properties from input + * Returns 0 on success, negative AVERROR on failure + */ +static int prepare_output_packet(AVBSFContext *ctx, AVPacket *pkt, + const AVPacket *in, int out_size) +{ + int ret = av_new_packet(pkt, out_size); + if (ret < 0) + return ret; + + ret = av_packet_copy_props(pkt, in); + if (ret < 0) { + av_packet_unref(pkt); + return ret; + } + return 0; +} + +/** + * Write data with emulation prevention bytes + * Returns number of bytes written + */ +static int write_escaped(uint8_t *dst, const uint8_t *src, int size) +{ + int i, j = 0; + for (i = 0; i < size; i++) { + if (i + 2 < size && src[i] == 0 && src[i + 1] == 0 && src[i + 2] <= 3) { + dst[j++] = 0; + dst[j++] = 0; + dst[j++] = 3; /* emulation_prevention_three_byte */ + dst[j++] = src[i + 2]; /* Write the third byte after insertion byte */ + i += 2; /* Skip the two zeros and the third byte */ + } else { + dst[j++] = src[i]; + } + } + return j; +} + +/** + * Remove emulation prevention bytes from escaped data + * Returns number of bytes written + */ +static int remove_escape(uint8_t *dst, const uint8_t *src, int size) +{ + int i, j = 0; + for (i = 0; i < size; i++) { + if (i + 2 < size && src[i] == 0 && src[i + 1] == 0 && src[i + 2] == 3) { + dst[j++] = 0; + dst[j++] = 0; + i += 2; /* skip the emulation_prevention_three_byte */ + } else { + dst[j++] = src[i]; + } + } + return j; +} + +/** + * Check if the first OBU in data is a valid Temporal Delimiter + * Returns 1 if a valid TD is present, 0 otherwise + * + * A valid Temporal Delimiter OBU must: + * - Have OBU type = AV1_OBU_TEMPORAL_DELIMITER (2) + * - Have has_size_field = 1 + * - Have size = 0 (LEB128 encoded) + */ +static int has_temporal_delimiter(const uint8_t *data, int size) +{ + if (size < 2) + return 0; + + /* OBU header: forbidden(1) | type(4) | extension(1) | has_size(1) | reserved(1) + * Temporal Delimiter type = 2, so header byte with has_size=1 is 0x12 + * with has_size=0 is 0x10 (but per spec, TD should have has_size=1) */ + int obu_type = (data[0] >> 3) & 0x0f; + int has_size = (data[0] >> 1) & 0x01; + + if (obu_type != AV1_OBU_TEMPORAL_DELIMITER) + return 0; + + /* Per AV1 spec, Temporal Delimiter should have has_size_field = 1 + * and should have zero payload size */ + if (!has_size) { + /* TD with has_size=0 is valid per spec but uncommon. + * In this case, the entire TD is just the header byte. */ + return 1; + } + + /* has_size=1, next byte(s) contain the size in LEB128 format. + * For TD, size must be 0, which is encoded as a single 0 byte. */ + if (data[1] != 0) { + /* Size is not zero, so this is not a valid Temporal Delimiter */ + return 0; + } + + return 1; +} + +/** + * Convert from Low Overhead Bitstream Format to Start Code Based Format + * (for muxing to MPEG-TS) + */ +static int av1_to_ts_filter(AVBSFContext *ctx, AVPacket *pkt) +{ + int ret; + const uint8_t *in_data; + int in_size; + uint8_t *out_data; + int out_size = 0; + int max_out_size; + AV1OBU obu; + AVPacket *in = NULL; + + /* Move input packet to 'in' */ + in = av_packet_alloc(); + if (!in) + return AVERROR(ENOMEM); + + ret = ff_bsf_get_packet_ref(ctx, in); + if (ret < 0) { + av_packet_free(&in); + return ret; + } + + in_data = in->data; + in_size = in->size; + + av_log(ctx, AV_LOG_DEBUG, "av1_to_ts: input size %d\n", in_size); + + /* Check if packet starts with Temporal Delimiter. + * If not, we'll insert one to ensure proper TU boundaries. + */ + int need_td = !has_temporal_delimiter(in_data, in_size); + if (need_td) { + av_log(ctx, AV_LOG_DEBUG, "Inserting Temporal Delimiter OBU\n"); + } + + /* Calculate maximum output size: + * Worst case: every byte could need escape, so 3/2 * original size (approx * 2) + * Plus overhead for start codes and TD insertion + */ + max_out_size = in_size * 2 + BUFFER_PADDING + (need_td ? (START_CODE_SIZE + sizeof(temporal_delimiter_obu)) : 0); + + ret = prepare_output_packet(ctx, pkt, in, max_out_size); + if (ret < 0) { + av_packet_free(&in); + return ret; + } + + out_data = pkt->data; + + /* Insert Temporal Delimiter if needed */ + if (need_td) { + /* Write start code for TD */ + out_data[out_size++] = 0; + out_data[out_size++] = 0; + out_data[out_size++] = 1; + /* Write TD OBU (no escaping needed - it's just 0x12 0x00) */ + memcpy(out_data + out_size, temporal_delimiter_obu, sizeof(temporal_delimiter_obu)); + out_size += sizeof(temporal_delimiter_obu); + } + + /* Parse and convert each OBU */ + while (in_size > 0) { + int obu_size; + int escaped_size; + + ret = ff_av1_extract_obu(&obu, in_data, in_size, ctx); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, "Failed to extract OBU\n"); + av_packet_unref(pkt); + av_packet_free(&in); + return ret; + } + + obu_size = obu.raw_size; + + /* Write start code */ + out_data[out_size++] = 0; + out_data[out_size++] = 0; + out_data[out_size++] = 1; + + /* Write OBU with emulation prevention */ + escaped_size = write_escaped(out_data + out_size, obu.raw_data, obu_size); + out_size += escaped_size; + + in_data += obu_size; + in_size -= obu_size; + } + + av_shrink_packet(pkt, out_size); + av_packet_free(&in); + return 0; +} + +/** + * Convert from Start Code Based Format to Low Overhead Bitstream Format + * (for demuxing from MPEG-TS) + */ +static int av1_from_ts_filter(AVBSFContext *ctx, AVPacket *pkt) +{ + int ret; + const uint8_t *in_data; + int in_size; + uint8_t *out_data; + int out_size = 0; + AVPacket *in = NULL; + + /* Allocate separate input packet to preserve data while we allocate output. */ + in = av_packet_alloc(); + if (!in) + return AVERROR(ENOMEM); + + ret = ff_bsf_get_packet_ref(ctx, in); + if (ret < 0) { + av_packet_free(&in); + return ret; + } + + in_data = in->data; + in_size = in->size; + + /* Output size will be smaller (no start codes, fewer escape bytes) */ + ret = prepare_output_packet(ctx, pkt, in, in_size + BUFFER_PADDING); + if (ret < 0) { + av_packet_free(&in); + return ret; + } + + out_data = pkt->data; + + /* Find and process each OBU */ + while (in_size > 0) { + const uint8_t *obu_start; + int obu_size; + int unescaped_size; + + /* Check for start code 0x000001 */ + if (in_size < MIN_PACKET_SIZE || in_data[0] != 0 || in_data[1] != 0 || in_data[2] != 1) { + av_log(ctx, AV_LOG_ERROR, "Missing start code at position %d\n", + (int)(in_data - pkt->data)); + ret = AVERROR_INVALIDDATA; + goto fail; + } + + in_data += START_CODE_SIZE; + in_size -= START_CODE_SIZE; + obu_start = in_data; + + /* Find next start code or end of data */ + obu_size = 0; + while (obu_size + START_CODE_SIZE <= in_size) { + if (in_data[obu_size] == 0 && in_data[obu_size + 1] == 0 && + in_data[obu_size + 2] == 1) { + break; + } + obu_size++; + } + if (obu_size + START_CODE_SIZE > in_size) { + obu_size = in_size; /* Last OBU */ + } + + /* Remove emulation prevention bytes */ + unescaped_size = remove_escape(out_data + out_size, obu_start, obu_size); + out_size += unescaped_size; + + in_data += obu_size; + in_size -= obu_size; + } + + av_shrink_packet(pkt, out_size); + av_packet_free(&in); + return 0; + +fail: + av_packet_unref(pkt); + av_packet_free(&in); + return ret; +} + +static int av1_ts_filter(AVBSFContext *ctx, AVPacket *pkt) +{ + AV1TSContext *s = ctx->priv_data; + + if (s->mode == 0) + return av1_to_ts_filter(ctx, pkt); + else + return av1_from_ts_filter(ctx, pkt); +} + +static const enum AVCodecID av1_ts_codec_ids[] = { + AV_CODEC_ID_AV1, AV_CODEC_ID_NONE, +}; + +#define OFFSET(x) offsetof(AV1TSContext, x) +#define FLAGS (AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_BSF_PARAM) + +static const AVOption av1_ts_options[] = { + { "mode", "Conversion mode", OFFSET(mode), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS, .unit = "mode" }, + { "to_ts", "Low Overhead to Start Code (for muxing)", 0, AV_OPT_TYPE_CONST, { .i64 = 0 }, 0, 0, FLAGS, .unit = "mode" }, + { "from_ts", "Start Code to Low Overhead (for demuxing)", 0, AV_OPT_TYPE_CONST, { .i64 = 1 }, 0, 0, FLAGS, .unit = "mode" }, + { NULL } +}; + +static const AVClass av1_ts_class = { + .class_name = "av1_ts", + .item_name = av_default_item_name, + .option = av1_ts_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +const FFBitStreamFilter ff_av1_ts_bsf = { + .p.name = "av1_ts", + .p.codec_ids = av1_ts_codec_ids, + .p.priv_class = &av1_ts_class, + .priv_data_size = sizeof(AV1TSContext), + .filter = av1_ts_filter, +}; -- 2.49.1 From 2320818875dda6da85e12da2925705dfc470aa3c Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:42:18 +0800 Subject: [PATCH 02/11] doc/bitstream_filters: add av1_ts documentation Document the new av1_ts bitstream filter, explaining its usage for converting between Annex B and Low Overhead formats. Signed-off-by: Jun Zhao <[email protected]> --- doc/bitstream_filters.texi | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/bitstream_filters.texi b/doc/bitstream_filters.texi index fb31ca7380..cb5e3512b7 100644 --- a/doc/bitstream_filters.texi +++ b/doc/bitstream_filters.texi @@ -92,6 +92,26 @@ Deletes Padding OBUs. @end table +@section av1_ts + +Convert AV1 bitstreams between Annex B (as used in MPEG-TS) and Low Overhead (standard) formats. + +@table @option +@item mode +Set the conversion mode. + +@table @samp +@item to_ts +Convert from Low Overhead to Annex B (MPEG-TS compatible). +Inserts Start Codes and applies emulation prevention bytes. +It also ensures a Temporal Delimiter OBU is present at the beginning of each packet. + +@item from_ts +Convert from Annex B to Low Overhead. +Strips Start Codes and removes emulation prevention bytes. +@end table +@end table + @section chomp Remove zero padding at the end of a packet. -- 2.49.1 From 8f99c66ad00573cfe78a768795df8c382609323e Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:43:45 +0800 Subject: [PATCH 03/11] lavf/mpegts: add AV1 demuxing support Add support for demuxing AV1 video from MPEG-2 Transport Streams according to the "AV1 Codec in MPEG-2 TS" specification. This implementation detects AV1 streams and automatically inserts the 'av1_ts' bitstream filter to convert the stream from the TS-compliant Annex B format to the Low Overhead format preferred by FFmpeg decoders. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/mpegts.c | 131 ++++++++++++++++++++++++++++++++++++++++++- libavformat/mpegts.h | 4 ++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/libavformat/mpegts.c b/libavformat/mpegts.c index 7c19abaf76..0d4b2fa370 100644 --- a/libavformat/mpegts.c +++ b/libavformat/mpegts.c @@ -36,6 +36,7 @@ #include "libavcodec/defs.h" #include "libavcodec/get_bits.h" #include "libavcodec/opus/opus.h" +#include "libavcodec/bsf.h" #include "avformat.h" #include "mpegts.h" #include "internal.h" @@ -182,6 +183,7 @@ struct MpegTSContext { AVStream *epg_stream; AVBufferPool* pools[32]; + int av1_low_overhead; }; #define MPEGTS_OPTIONS \ @@ -193,7 +195,10 @@ struct MpegTSContext { { .i64 = 0 }, 0, INT_MAX, AV_OPT_FLAG_EXPORT | AV_OPT_FLAG_READONLY }, \ { "ts_packetsize", "output option carrying the raw packet size", \ offsetof(MpegTSContext, raw_packet_size), AV_OPT_TYPE_INT, \ - { .i64 = 0 }, 0, INT_MAX, AV_OPT_FLAG_EXPORT | AV_OPT_FLAG_READONLY } + { .i64 = 0 }, 0, INT_MAX, AV_OPT_FLAG_EXPORT | AV_OPT_FLAG_READONLY }, \ + { "av1_low_overhead", "Export AV1 in Low Overhead format (default: true)", \ + offsetof(MpegTSContext, av1_low_overhead), AV_OPT_TYPE_BOOL, \ + { .i64 = 1 }, 0, 1, AV_OPT_FLAG_DECODING_PARAM } static const AVOption options[] = { MPEGTS_OPTIONS, @@ -273,6 +278,8 @@ typedef struct PESContext { AVBufferRef *buffer; SLConfigDescr sl; int merged_st; + AVBSFContext *bsf; + AVPacket *bsf_pkt; } PESContext; EXTERN const FFInputFormat ff_mpegts_demuxer; @@ -569,6 +576,8 @@ static void mpegts_close_filter(MpegTSContext *ts, MpegTSFilter *filter) else if (filter->type == MPEGTS_PES) { PESContext *pes = filter->u.pes_filter.opaque; av_buffer_unref(&pes->buffer); + av_bsf_free(&pes->bsf); + av_packet_free(&pes->bsf_pkt); /* referenced private data will be freed later in * avformat_close_input (pes->st->priv_data == pes) */ if (!pes->st || pes->merged_st) { @@ -864,6 +873,7 @@ static const StreamType HLS_SAMPLE_ENC_types[] = { }; static const StreamType REGD_types[] = { + { MKTAG('A', 'V', '0', '1'), AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_AV1 }, { MKTAG('d', 'r', 'a', 'c'), AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_DIRAC }, { MKTAG('A', 'C', '-', '3'), AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_AC3 }, { MKTAG('A', 'C', '-', '4'), AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_AC4 }, @@ -1013,6 +1023,31 @@ static void new_data_packet(const uint8_t *buffer, int len, AVPacket *pkt) pkt->size = len; } +static void mpegts_ensure_av1_bsf(PESContext *pes) +{ + if (!pes->ts->av1_low_overhead || pes->bsf || !pes->st || pes->st->codecpar->codec_id != AV_CODEC_ID_AV1) + return; + + const AVBitStreamFilter *filter = av_bsf_get_by_name("av1_ts"); + if (filter) { + int ret; + if ((ret = av_bsf_alloc(filter, &pes->bsf)) >= 0) { + avcodec_parameters_copy(pes->bsf->par_in, pes->st->codecpar); + pes->bsf->time_base_in = pes->st->time_base; + if ((ret = av_opt_set(pes->bsf->priv_data, "mode", "from_ts", 0)) < 0) + av_log(pes->stream, AV_LOG_WARNING, "Failed to set av1_ts mode to from_ts: %d\n", ret); + + if ((ret = av_bsf_init(pes->bsf)) < 0) { + av_bsf_free(&pes->bsf); + } else { + pes->bsf_pkt = av_packet_alloc(); + if (!pes->bsf_pkt) + av_bsf_free(&pes->bsf); + } + } + } +} + static int new_pes_packet(PESContext *pes, AVPacket *pkt) { uint8_t *sd; @@ -1057,8 +1092,33 @@ static int new_pes_packet(PESContext *pes, AVPacket *pkt) pkt->dts = pes->dts; /* store position of first TS packet of this PES packet */ pkt->pos = pes->ts_packet_pos; + + mpegts_ensure_av1_bsf(pes); + pkt->flags = pes->flags; + if (pes->bsf) { + int stream_index = pkt->stream_index; + int64_t pos = pkt->pos; + int ret = av_bsf_send_packet(pes->bsf, pkt); + if (ret < 0) { + av_log(pes->stream, AV_LOG_WARNING, "Error sending to av1_ts: %d\n", ret); + pes->buffer = NULL; + return ret; + } + + ret = av_bsf_receive_packet(pes->bsf, pkt); + if (ret < 0) { + pes->buffer = NULL; + if (ret == AVERROR(EAGAIN)) + return 0; + av_log(pes->stream, AV_LOG_WARNING, "Error receiving from av1_ts: %d\n", ret); + return ret; + } + pkt->stream_index = stream_index; + pkt->pos = pos; + } + pes->buffer = NULL; reset_pes_packet_state(pes); @@ -2294,6 +2354,75 @@ int ff_parse_mpeg2_descriptor(AVFormatContext *fc, AVStream *st, int stream_type sti->need_parsing = 0; } break; + case AV1_VIDEO_DESCRIPTOR: + /* AV1 video descriptor per "AV1 Codec in MPEG-2 TS" spec + * https://aomediacodec.github.io/av1-mpeg2-ts/ + * This descriptor is used in conjunction with registration descriptor 'AV01' + */ + if (st->codecpar->codec_id == AV_CODEC_ID_AV1) { + int marker_version, seq_profile, seq_level_idx_0, seq_tier_0; + int high_bitdepth, twelve_bit, monochrome; + int chroma_subsampling_x, chroma_subsampling_y, chroma_sample_position; + int hdr_wcg_idc; + uint8_t byte0, byte1, byte2, byte3; + + if (desc_end - *pp < 4) + return AVERROR_INVALIDDATA; + + byte0 = get8(pp, desc_end); /* marker(1) | version(7) */ + byte1 = get8(pp, desc_end); /* seq_profile(3) | seq_level_idx_0(5) */ + byte2 = get8(pp, desc_end); /* seq_tier_0(1) | high_bitdepth(1) | twelve_bit(1) | monochrome(1) | chroma_subsampling_x(1) | chroma_subsampling_y(1) | chroma_sample_position(2) */ + byte3 = get8(pp, desc_end); /* hdr_wcg_idc(2) | reserved(1) | initial_presentation_delay_present(1) | initial_presentation_delay(4) */ + + marker_version = byte0; + if ((marker_version & 0x80) != 0x80) { + av_log(fc, AV_LOG_WARNING, "Invalid AV1 video descriptor marker\n"); + break; + } + + seq_profile = (byte1 >> 5) & 0x07; + seq_level_idx_0 = byte1 & 0x1f; + seq_tier_0 = (byte2 >> 7) & 0x01; + high_bitdepth = (byte2 >> 6) & 0x01; + twelve_bit = (byte2 >> 5) & 0x01; + monochrome = (byte2 >> 4) & 0x01; + chroma_subsampling_x = (byte2 >> 3) & 0x01; + chroma_subsampling_y = (byte2 >> 2) & 0x01; + chroma_sample_position = byte2 & 0x03; + hdr_wcg_idc = (byte3 >> 6) & 0x03; + + st->codecpar->profile = seq_profile; + st->codecpar->level = seq_level_idx_0 | (seq_tier_0 << 8); + + /* Derive bit depth */ + if (seq_profile == 2 && high_bitdepth) + st->codecpar->bits_per_coded_sample = twelve_bit ? 12 : 10; + else if (seq_profile <= 2) + st->codecpar->bits_per_coded_sample = high_bitdepth ? 10 : 8; + + /* Set chroma location based on chroma_sample_position */ + if (!monochrome && chroma_subsampling_x && chroma_subsampling_y) { + switch (chroma_sample_position) { + case 1: + st->codecpar->chroma_location = AVCHROMA_LOC_LEFT; + break; + case 2: + st->codecpar->chroma_location = AVCHROMA_LOC_TOPLEFT; + break; + } + } + + av_log(fc, AV_LOG_TRACE, "AV1 video descriptor: profile=%d, level=%d, tier=%d, " + "bit_depth=%d, monochrome=%d, chroma_subsampling=%dx%d, " + "chroma_sample_position=%d, hdr_wcg_idc=%d\n", + seq_profile, seq_level_idx_0, seq_tier_0, + st->codecpar->bits_per_coded_sample, monochrome, + chroma_subsampling_x, chroma_subsampling_y, + chroma_sample_position, hdr_wcg_idc); + + sti->need_context_update = 1; + } + break; case DOVI_VIDEO_STREAM_DESCRIPTOR: { uint32_t buf; diff --git a/libavformat/mpegts.h b/libavformat/mpegts.h index 223962d18e..a81e3b3f16 100644 --- a/libavformat/mpegts.h +++ b/libavformat/mpegts.h @@ -230,6 +230,10 @@ https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/ https://professional.dolby.com/siteassets/content-creation/dolby-vision-for-content-creators/dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2.pdf */ #define DOVI_VIDEO_STREAM_DESCRIPTOR 0xb0 +/** see "AV1 Codec in MPEG-2 Transport Stream" +https://aomediacodec.github.io/av1-mpeg2-ts/ */ +#define AV1_VIDEO_DESCRIPTOR 0x80 + #define DATA_COMPONENT_DESCRIPTOR 0xfd /* ARIB STD-B10 */ typedef struct MpegTSContext MpegTSContext; -- 2.49.1 From 73a7a72ecce158005d1d8b65c5d2022bb9826c65 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:43:45 +0800 Subject: [PATCH 04/11] doc/demuxers: add av1_low_overhead documentation Document the av1_low_overhead option for the MPEG-TS demuxer, which controls the automatic insertion of the av1_ts bitstream filter. Signed-off-by: Jun Zhao <[email protected]> --- doc/demuxers.texi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/demuxers.texi b/doc/demuxers.texi index c1dda7f1eb..0eeb8c4878 100644 --- a/doc/demuxers.texi +++ b/doc/demuxers.texi @@ -1001,6 +1001,10 @@ streams move to different PIDs. Default value is 0. @item max_packet_size Set maximum size, in bytes, of packet emitted by the demuxer. Payloads above this size are split across multiple packets. Range is 1 to INT_MAX/2. Default is 204800 bytes. + +@item av1_low_overhead +Convert AV1 stream into low-overhead format. +This setting is enabled by default. If disabled, the stream will be exported in Annex B format. @end table @section mpjpeg -- 2.49.1 From 276faba2b67191b00e0d40a131fb1d08e9124e18 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 17:31:03 +0800 Subject: [PATCH 05/11] lavf/mpegtsenc: add AV1 muxing support Add support for muxing AV1 video into MPEG-2 Transport Streams as defined in the "AV1 Codec in MPEG-2 TS" specification. It automatically inserts the av1_ts bitstream filter to convert the internal Low Overhead format to the Annex B format required by the MPEG-TS standard. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/mpegtsenc.c | 132 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/libavformat/mpegtsenc.c b/libavformat/mpegtsenc.c index ea7c6065a0..3497c956b7 100644 --- a/libavformat/mpegtsenc.c +++ b/libavformat/mpegtsenc.c @@ -27,6 +27,7 @@ #include "libavutil/mathematics.h" #include "libavutil/mem.h" #include "libavutil/opt.h" +#include "libavutil/pixdesc.h" #include "libavcodec/ac3_parser_internal.h" #include "libavcodec/bytestream.h" @@ -374,6 +375,9 @@ static int get_dvb_stream_type(AVFormatContext *s, AVStream *st) case AV_CODEC_ID_VVC: stream_type = STREAM_TYPE_VIDEO_VVC; break; + case AV_CODEC_ID_AV1: + stream_type = STREAM_TYPE_PRIVATE_DATA; + break; case AV_CODEC_ID_CAVS: stream_type = STREAM_TYPE_VIDEO_CAVS; break; @@ -810,6 +814,99 @@ static int mpegts_write_pmt(AVFormatContext *s, MpegTSService *service) } else if (stream_type == STREAM_TYPE_VIDEO_CAVS || stream_type == STREAM_TYPE_VIDEO_AVS2 || stream_type == STREAM_TYPE_VIDEO_AVS3) { put_registration_descriptor(&q, MKTAG('A', 'V', 'S', 'V')); + } else if (codec_id == AV_CODEC_ID_AV1) { + /* AV1 uses PRIVATE_DATA stream type with 'AV01' registration descriptor + * and AV1 video descriptor per https://aomediacodec.github.io/av1-mpeg2-ts/ + */ + int seq_profile = st->codecpar->profile >= 0 ? st->codecpar->profile : 0; + int seq_level_idx_0 = st->codecpar->level >= 0 ? (st->codecpar->level & 0x1f) : 1; + int seq_tier_0 = st->codecpar->level >= 0 ? ((st->codecpar->level >> 8) & 0x01) : 0; + int high_bitdepth = 0, twelve_bit = 0, monochrome = 0; + int chroma_subsampling_x = 1, chroma_subsampling_y = 1; + int chroma_sample_position = 0; + int hdr_wcg_idc = 0; + + /* Derive high_bitdepth and twelve_bit from bits_per_coded_sample */ + if (st->codecpar->bits_per_coded_sample == 10) { + high_bitdepth = 1; + } else if (st->codecpar->bits_per_coded_sample == 12) { + high_bitdepth = 1; + twelve_bit = 1; + } + + /* Derive chroma subsampling from pixel format */ + if (st->codecpar->format != AV_PIX_FMT_NONE) { + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(st->codecpar->format); + if (desc) { + if (desc->nb_components == 1) { + monochrome = 1; + chroma_subsampling_x = 1; + chroma_subsampling_y = 1; + } else { + chroma_subsampling_x = desc->log2_chroma_w ? 1 : 0; + chroma_subsampling_y = desc->log2_chroma_h ? 1 : 0; + } + } + } + + /* Derive chroma_sample_position from chroma_location */ + switch (st->codecpar->chroma_location) { + case AVCHROMA_LOC_LEFT: + chroma_sample_position = 1; + break; + case AVCHROMA_LOC_TOPLEFT: + chroma_sample_position = 2; + break; + default: + chroma_sample_position = 0; + break; + } + + /* Derive hdr_wcg_idc from color properties: + * 0 = SDR + * 1 = WCG only (Wide Color Gamut without HDR) + * 2 = HDR and WCG (both HDR transfer and wide gamut) + * 3 = No indication + * + * HDR is indicated by PQ (SMPTE ST 2084) or HLG (ARIB STD-B67) transfer. + * WCG is indicated by BT.2020 color primaries. + */ + { + int is_hdr = (st->codecpar->color_trc == AVCOL_TRC_SMPTE2084 || + st->codecpar->color_trc == AVCOL_TRC_ARIB_STD_B67); + int is_wcg = (st->codecpar->color_primaries == AVCOL_PRI_BT2020); + + if (is_hdr && is_wcg) { + hdr_wcg_idc = 2; /* HDR and WCG */ + } else if (is_hdr) { + /* HDR without WCG primaries - still signal as HDR+WCG since + * HDR content typically implies wide gamut even if not explicitly + * tagged with BT.2020 primaries */ + hdr_wcg_idc = 2; + } else if (is_wcg) { + hdr_wcg_idc = 1; /* WCG only (SDR with wide gamut) */ + } else { + hdr_wcg_idc = 0; /* SDR */ + } + + av_log(s, AV_LOG_DEBUG, "AV1 stream %d: color_trc=%d, color_primaries=%d, " + "is_hdr=%d, is_wcg=%d, hdr_wcg_idc=%d\n", + i, st->codecpar->color_trc, st->codecpar->color_primaries, + is_hdr, is_wcg, hdr_wcg_idc); + } + + /* Registration descriptor 'AV01' - must come first */ + put_registration_descriptor(&q, MKTAG('A', 'V', '0', '1')); + + /* AV1 video descriptor */ + *q++ = AV1_VIDEO_DESCRIPTOR; /* descriptor_tag */ + *q++ = 4; /* descriptor_length */ + *q++ = 0x81; /* marker(1) | version(7) = 1 */ + *q++ = (seq_profile << 5) | (seq_level_idx_0 & 0x1f); + *q++ = (seq_tier_0 << 7) | (high_bitdepth << 6) | (twelve_bit << 5) | + (monochrome << 4) | (chroma_subsampling_x << 3) | + (chroma_subsampling_y << 2) | (chroma_sample_position & 0x03); + *q++ = (hdr_wcg_idc << 6); /* hdr_wcg_idc(2) | reserved(1)=0 | initial_presentation_delay_present(1)=0 | reserved(4)=0 */ } break; case AVMEDIA_TYPE_DATA: @@ -1453,6 +1550,8 @@ static int get_pes_stream_id(AVFormatContext *s, AVStream *st, int stream_id, in if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { if (st->codecpar->codec_id == AV_CODEC_ID_DIRAC) return STREAM_ID_EXTENDED_STREAM_ID; + else if (st->codecpar->codec_id == AV_CODEC_ID_AV1) + return STREAM_ID_PRIVATE_STREAM_1; else return STREAM_ID_VIDEO_STREAM_0; } else if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && @@ -1673,8 +1772,10 @@ static void mpegts_write_pes(AVFormatContext *s, AVStream *st, *q++ = len >> 8; *q++ = len; val = 0x80; - /* data alignment indicator is required for subtitle and data streams */ - if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE || st->codecpar->codec_type == AVMEDIA_TYPE_DATA) + /* data alignment indicator is required for subtitle, data streams, and AV1 */ + if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE || + st->codecpar->codec_type == AVMEDIA_TYPE_DATA || + st->codecpar->codec_id == AV_CODEC_ID_AV1) val |= 0x04; *q++ = val; *q++ = flags; @@ -2325,6 +2426,33 @@ static int mpegts_check_bitstream(AVFormatContext *s, AVStream *st, ((st->codecpar->extradata[0] & e->mask) == e->value)))) return ff_stream_add_bitstream_filter(st, e->bsf_name, NULL); } + + /* AV1 in MPEG-TS uses Start Code Based Format (0x000001 prefix per OBU). + * If input is Low Overhead Bitstream Format (no start codes), insert av1_ts BSF. + */ + if (st->codecpar->codec_id == AV_CODEC_ID_AV1) { + int is_low_overhead = 0; + + /* Check extradata for AV1CodecConfigurationRecord (starts with 0x81) */ + if (st->codecpar->extradata_size >= 4 && (st->codecpar->extradata[0] & 0x80)) { + is_low_overhead = 1; + av_log(s, AV_LOG_DEBUG, "Stream %d: AV1CodecConfigurationRecord found in extradata, assuming Low Overhead\n", st->index); + } else if (pkt->size >= 4) { + /* Check if data starts with start code 0x000001 */ + if (AV_RB24(pkt->data) != 0x000001) { + is_low_overhead = 1; + av_log(s, AV_LOG_DEBUG, "Stream %d: No start code found in packet, assuming Low Overhead\n", st->index); + } + } + + if (is_low_overhead) { + av_log(s, AV_LOG_INFO, "Auto-inserting av1_ts BSF for stream %d\n", st->index); + return ff_stream_add_bitstream_filter(st, "av1_ts", "mode=to_ts"); + } else { + av_log(s, AV_LOG_DEBUG, "Stream %d: Start code found or Annex B implied, no BSF needed\n", st->index); + } + } + return 1; } -- 2.49.1 From 4de631e8ddc9a43393570a7465fb99d503c485d0 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 17:31:15 +0800 Subject: [PATCH 06/11] doc: update Changelog for AV1 in MPEG-TS --- Changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog b/Changelog index 569c7ffad8..52667dc118 100644 --- a/Changelog +++ b/Changelog @@ -18,6 +18,7 @@ version <next>: - JPEG-XS parser - JPEG-XS decoder and encoder through libsvtjpegxs - JPEG-XS raw bitstream muxer and demuxer +- AV1 in MPEG-TS muxer and demuxer version 8.0: -- 2.49.1 From 2911ce96a35713273b7f5b0a0cad831f509c0d8a Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 17:15:07 +0800 Subject: [PATCH 07/11] doc/muxers: add AV1 MPEG-TS documentation Add documentation for the AV1 muxing support in MPEG-TS. Signed-off-by: Jun Zhao <[email protected]> --- doc/muxers.texi | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/muxers.texi b/doc/muxers.texi index a2e356187a..49c8324f1b 100644 --- a/doc/muxers.texi +++ b/doc/muxers.texi @@ -3056,6 +3056,26 @@ and @code{service_name}. If they are not set the default for @code{service_provider} is @samp{FFmpeg} and the default for @code{service_name} is @samp{Service01}. +@subsection AV1 Streams + +AV1 streams are automatically converted from low-overhead bitstream format +(as used in MP4/WebM containers) to start code based format (Annex B) as +required by the MPEG-TS specification. The conversion is handled internally +by the @code{av1_ts} bitstream filter and is transparent to the user. + +The HDR/WCG indicator in the AV1 video descriptor is automatically derived +from the stream's color parameters. The @code{hdr_wcg_idc} field in the PMT +AV1 descriptor is set according to transfer characteristics and color +primaries: +@table @samp +@item 0 = SDR (Standard Dynamic Range) +No HDR or Wide Color Gamut transfer functions or primaries detected. +@item 1 = WCG (Wide Color Gamut) +Wide Color Gamut primaries (e.g., BT.2020) detected, but no HDR transfer function. +@item 2 = HDR+WCG +Both HDR transfer function (PQ or HLG) and Wide Color Gamut primaries detected. +@end table + @subsection Options The muxer options are: -- 2.49.1 From f2dd4d50c4739e8afacb9550cbf09c508e3f582d Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:20:23 +0800 Subject: [PATCH 08/11] lavf/movenc: use av1_ts bsf Use the av1_ts bitstream filter to handle AV1 streams, ensuring proper conversion from Annex B to the format required by ISOBMFF (MP4/MOV) when necessary. ISOBMFF requires the Low Overhead format (av1c). If the input is in Annex B (e.g. from MPEG-TS), this ensures it is correctly converted. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/movenc.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libavformat/movenc.c b/libavformat/movenc.c index 8d8acd2aff..d81fc4f55c 100644 --- a/libavformat/movenc.c +++ b/libavformat/movenc.c @@ -8799,6 +8799,13 @@ static int mov_check_bitstream(AVFormatContext *s, AVStream *st, ret = ff_stream_add_bitstream_filter(st, "aac_adtstoasc", NULL); } else if (st->codecpar->codec_id == AV_CODEC_ID_VP9) { ret = ff_stream_add_bitstream_filter(st, "vp9_superframe", NULL); + } else if (st->codecpar->codec_id == AV_CODEC_ID_AV1) { + /* AV1 in MP4/MOV uses Low Overhead Bitstream Format (no start codes). + * If input is Start Code Based Format (from MPEG-TS), convert it. + */ + if (pkt->size >= 4 && AV_RB24(pkt->data) == 0x000001) { + ret = ff_stream_add_bitstream_filter(st, "av1_ts", "mode=from_ts"); + } } return ret; -- 2.49.1 From 1b95c1fac10e3e3e82574cbd30af6cc97522fb43 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:20:30 +0800 Subject: [PATCH 09/11] lavf/flvenc: use av1_ts bsf Use the av1_ts bitstream filter for AV1 in FLV/RTMP to ensure compliance with the Enhanced RTMP/FLV specification for AV1 encapsulation. Similar to ISOBMFF, Enhanced RTMP expects the packetized (Low Overhead) format. This change ensures any Annex B input is converted. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/flvenc.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libavformat/flvenc.c b/libavformat/flvenc.c index a0503c1799..d221eed32c 100644 --- a/libavformat/flvenc.c +++ b/libavformat/flvenc.c @@ -1473,6 +1473,13 @@ static int flv_check_bitstream(AVFormatContext *s, AVStream *st, if (pkt->size > 2 && (AV_RB16(pkt->data) & 0xfff0) == 0xfff0) return ff_stream_add_bitstream_filter(st, "aac_adtstoasc", NULL); } + if (st->codecpar->codec_id == AV_CODEC_ID_AV1 && pkt->size >= 4) { + /* AV1 in FLV uses Low Overhead Bitstream Format (no start codes). + * If input is Start Code Based Format (0x000001 prefix per OBU), + * insert av1_ts BSF to convert it. */ + if (AV_RB24(pkt->data) == 0x000001) + return ff_stream_add_bitstream_filter(st, "av1_ts", "mode=from_ts"); + } if (!st->codecpar->extradata_size && (st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_HEVC || -- 2.49.1 From 920dff1491695f77f6349c064efc088d43ef6679 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 14:20:37 +0800 Subject: [PATCH 10/11] tests: add av1_ts bitstream filter API test Add a test covering the API usage of the av1_ts bitstream filter, verifying memory handling, basic conversion logic, and round-trip consistency between Annex B and Low Overhead formats. Signed-off-by: Jun Zhao <[email protected]> --- tests/api/Makefile | 2 +- tests/api/api-av1-ts-bsf-test.c | 480 ++++++++++++++++++++++++++++++++ tests/fate/api.mak | 5 + 3 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 tests/api/api-av1-ts-bsf-test.c diff --git a/tests/api/Makefile b/tests/api/Makefile index 899aeb1f54..e5b738f30e 100644 --- a/tests/api/Makefile +++ b/tests/api/Makefile @@ -1,7 +1,7 @@ APITESTPROGS-$(call ENCDEC, FLAC, FLAC) += api-flac APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264-slice -APITESTPROGS-yes += api-seek api-dump-stream-meta +APITESTPROGS-yes += api-seek api-dump-stream-meta api-av1-ts-bsf APITESTPROGS-$(call DEMDEC, H263, H263) += api-band APITESTPROGS-$(HAVE_THREADS) += api-threadmessage APITESTPROGS += $(APITESTPROGS-yes) diff --git a/tests/api/api-av1-ts-bsf-test.c b/tests/api/api-av1-ts-bsf-test.c new file mode 100644 index 0000000000..3d1fe2cec3 --- /dev/null +++ b/tests/api/api-av1-ts-bsf-test.c @@ -0,0 +1,480 @@ +/* + * Test for av1_ts bitstream filter memory handling + * + * This test verifies that the av1_ts BSF correctly handles memory + * when converting between Low Overhead and Start Code formats. + * + * Bug being tested: In av1_from_ts_filter, the original implementation + * stored a pointer to pkt->data in in_data, then called av_new_packet() + * which frees pkt and allocates new memory, leaving in_data as a + * dangling pointer. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "libavcodec/avcodec.h" +#include "libavcodec/bsf.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +/* Minimal AV1 OBU header for testing: + * - OBU type = 1 (sequence header) + * - obu_extension_flag = 0 + * - obu_has_size_field = 1 + * - obu_size = 2 (LEB128 encoded) + * - 2 bytes of dummy payload + */ +static const uint8_t low_overhead_obu[] = { + 0x0A, /* OBU header: type=1, ext=0, has_size=1 */ + 0x02, /* OBU size (LEB128): 2 bytes */ + 0x00, 0x00 /* Dummy payload */ +}; + +/* Same OBU in Start Code format (Annex B): + * - 3-byte start code 0x000001 + * - Same OBU data + */ +static const uint8_t start_code_obu[] = { + 0x00, 0x00, 0x01, /* Start code */ + 0x0A, /* OBU header */ + 0x02, /* OBU size */ + 0x00, 0x00 /* Dummy payload */ +}; + +/* Test OBU with data that could trigger emulation prevention: + * Contains 0x00 0x00 0x01 pattern that needs escaping + * Includes Temporal Delimiter at start to prevent auto-insertion + */ +static const uint8_t obu_with_escape_pattern[] = { + 0x12, 0x00, /* Temporal Delimiter OBU (type=2, size=0) */ + 0x0A, /* OBU header */ + 0x05, /* OBU size: 5 bytes */ + 0xAA, 0x00, 0x00, 0x01, 0xBB /* Payload with 0x000001 pattern */ +}; + +/* Same OBU after escaping (with emulation prevention byte 0x03) */ +static const uint8_t escaped_obu[] = { + 0x00, 0x00, 0x01, /* Start code */ + 0x12, 0x00, /* Temporal Delimiter OBU */ + 0x00, 0x00, 0x01, /* Start code */ + 0x0A, /* OBU header */ + 0x05, /* OBU size */ + 0xAA, 0x00, 0x00, 0x03, 0x01, 0xBB /* Escaped payload */ +}; + +static int test_to_ts_mode(void) +{ + const AVBitStreamFilter *filter; + AVBSFContext *bsf_ctx = NULL; + AVPacket *pkt_in = NULL; + AVPacket *pkt_out = NULL; + int ret; + + printf("Testing to_ts mode (Low Overhead -> Start Code)...\n"); + + filter = av_bsf_get_by_name("av1_ts"); + if (!filter) { + fprintf(stderr, "av1_ts BSF not found\n"); + return -1; + } + + ret = av_bsf_alloc(filter, &bsf_ctx); + if (ret < 0) { + fprintf(stderr, "Failed to allocate BSF context\n"); + return ret; + } + + /* Configure for to_ts mode */ + av_opt_set(bsf_ctx->priv_data, "mode", "to_ts", 0); + + bsf_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(bsf_ctx); + if (ret < 0) { + fprintf(stderr, "Failed to init BSF\n"); + av_bsf_free(&bsf_ctx); + return ret; + } + + pkt_in = av_packet_alloc(); + pkt_out = av_packet_alloc(); + if (!pkt_in || !pkt_out) { + ret = AVERROR(ENOMEM); + goto cleanup; + } + + /* Create input packet with Low Overhead data */ + ret = av_new_packet(pkt_in, sizeof(low_overhead_obu)); + if (ret < 0) + goto cleanup; + memcpy(pkt_in->data, low_overhead_obu, sizeof(low_overhead_obu)); + + /* Send to BSF */ + ret = av_bsf_send_packet(bsf_ctx, pkt_in); + if (ret < 0) { + fprintf(stderr, "Failed to send packet: %d\n", ret); + goto cleanup; + } + + /* Receive filtered packet */ + ret = av_bsf_receive_packet(bsf_ctx, pkt_out); + if (ret < 0) { + fprintf(stderr, "Failed to receive packet: %d\n", ret); + goto cleanup; + } + + /* Verify output has start code */ + if (pkt_out->size < 3 || + pkt_out->data[0] != 0x00 || + pkt_out->data[1] != 0x00 || + pkt_out->data[2] != 0x01) { + fprintf(stderr, "Output missing start code\n"); + ret = -1; + goto cleanup; + } + + printf(" to_ts mode: PASSED (output size: %d, has start code)\n", pkt_out->size); + ret = 0; + +cleanup: + av_packet_free(&pkt_in); + av_packet_free(&pkt_out); + av_bsf_free(&bsf_ctx); + return ret; +} + +static int test_from_ts_mode(void) +{ + const AVBitStreamFilter *filter; + AVBSFContext *bsf_ctx = NULL; + AVPacket *pkt_in = NULL; + AVPacket *pkt_out = NULL; + int ret; + + printf("Testing from_ts mode (Start Code -> Low Overhead)...\n"); + + filter = av_bsf_get_by_name("av1_ts"); + if (!filter) { + fprintf(stderr, "av1_ts BSF not found\n"); + return -1; + } + + ret = av_bsf_alloc(filter, &bsf_ctx); + if (ret < 0) { + fprintf(stderr, "Failed to allocate BSF context\n"); + return ret; + } + + /* Configure for from_ts mode */ + av_opt_set(bsf_ctx->priv_data, "mode", "from_ts", 0); + + bsf_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(bsf_ctx); + if (ret < 0) { + fprintf(stderr, "Failed to init BSF\n"); + av_bsf_free(&bsf_ctx); + return ret; + } + + pkt_in = av_packet_alloc(); + pkt_out = av_packet_alloc(); + if (!pkt_in || !pkt_out) { + ret = AVERROR(ENOMEM); + goto cleanup; + } + + /* Create input packet with Start Code data */ + ret = av_new_packet(pkt_in, sizeof(start_code_obu)); + if (ret < 0) + goto cleanup; + memcpy(pkt_in->data, start_code_obu, sizeof(start_code_obu)); + + /* Send to BSF */ + ret = av_bsf_send_packet(bsf_ctx, pkt_in); + if (ret < 0) { + fprintf(stderr, "Failed to send packet: %d\n", ret); + goto cleanup; + } + + /* Receive filtered packet - THIS IS WHERE THE BUG WOULD MANIFEST + * If in_data becomes a dangling pointer after av_new_packet(), + * this will either crash or produce garbage output. + */ + ret = av_bsf_receive_packet(bsf_ctx, pkt_out); + if (ret < 0) { + fprintf(stderr, "Failed to receive packet: %d\n", ret); + goto cleanup; + } + + /* Verify output matches expected Low Overhead format */ + if (pkt_out->size != sizeof(low_overhead_obu)) { + fprintf(stderr, "Output size mismatch: expected %zu, got %d\n", + sizeof(low_overhead_obu), pkt_out->size); + ret = -1; + goto cleanup; + } + + if (memcmp(pkt_out->data, low_overhead_obu, sizeof(low_overhead_obu)) != 0) { + fprintf(stderr, "Output data mismatch\n"); + printf(" Expected: "); + for (size_t i = 0; i < sizeof(low_overhead_obu); i++) + printf("%02x ", low_overhead_obu[i]); + printf("\n Got: "); + for (int i = 0; i < pkt_out->size; i++) + printf("%02x ", pkt_out->data[i]); + printf("\n"); + ret = -1; + goto cleanup; + } + + printf(" from_ts mode: PASSED (output size: %d, data matches)\n", pkt_out->size); + ret = 0; + +cleanup: + av_packet_free(&pkt_in); + av_packet_free(&pkt_out); + av_bsf_free(&bsf_ctx); + return ret; +} + +static int test_escape_handling(void) +{ + const AVBitStreamFilter *filter; + AVBSFContext *bsf_ctx = NULL; + AVPacket *pkt_in = NULL; + AVPacket *pkt_out = NULL; + int ret; + + printf("Testing emulation prevention byte handling...\n"); + + filter = av_bsf_get_by_name("av1_ts"); + if (!filter) { + fprintf(stderr, "av1_ts BSF not found\n"); + return -1; + } + + /* Test to_ts: should add escape bytes */ + ret = av_bsf_alloc(filter, &bsf_ctx); + if (ret < 0) + return ret; + + av_opt_set(bsf_ctx->priv_data, "mode", "to_ts", 0); + bsf_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(bsf_ctx); + if (ret < 0) { + av_bsf_free(&bsf_ctx); + return ret; + } + + pkt_in = av_packet_alloc(); + pkt_out = av_packet_alloc(); + if (!pkt_in || !pkt_out) { + ret = AVERROR(ENOMEM); + goto cleanup; + } + + ret = av_new_packet(pkt_in, sizeof(obu_with_escape_pattern)); + if (ret < 0) + goto cleanup; + memcpy(pkt_in->data, obu_with_escape_pattern, sizeof(obu_with_escape_pattern)); + + ret = av_bsf_send_packet(bsf_ctx, pkt_in); + if (ret < 0) + goto cleanup; + + ret = av_bsf_receive_packet(bsf_ctx, pkt_out); + if (ret < 0) + goto cleanup; + + /* Verify escape byte was inserted */ + if (pkt_out->size != sizeof(escaped_obu)) { + fprintf(stderr, "Escape test: size mismatch, expected %zu got %d\n", + sizeof(escaped_obu), pkt_out->size); + fprintf(stderr, " Expected: "); + for (size_t i = 0; i < sizeof(escaped_obu); i++) + fprintf(stderr, "%02x ", escaped_obu[i]); + fprintf(stderr, "\n Got: "); + for (int i = 0; i < pkt_out->size; i++) + fprintf(stderr, "%02x ", pkt_out->data[i]); + fprintf(stderr, "\n"); + ret = -1; + goto cleanup; + } + + if (memcmp(pkt_out->data, escaped_obu, sizeof(escaped_obu)) != 0) { + fprintf(stderr, "Escape test: data mismatch\n"); + ret = -1; + goto cleanup; + } + + printf(" Escape insertion: PASSED\n"); + + /* Now test from_ts: should remove escape bytes */ + av_packet_unref(pkt_in); + av_packet_unref(pkt_out); + av_bsf_free(&bsf_ctx); + + ret = av_bsf_alloc(filter, &bsf_ctx); + if (ret < 0) + return ret; + + av_opt_set(bsf_ctx->priv_data, "mode", "from_ts", 0); + bsf_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(bsf_ctx); + if (ret < 0) { + av_bsf_free(&bsf_ctx); + return ret; + } + + ret = av_new_packet(pkt_in, sizeof(escaped_obu)); + if (ret < 0) + goto cleanup; + memcpy(pkt_in->data, escaped_obu, sizeof(escaped_obu)); + + ret = av_bsf_send_packet(bsf_ctx, pkt_in); + if (ret < 0) + goto cleanup; + + ret = av_bsf_receive_packet(bsf_ctx, pkt_out); + if (ret < 0) + goto cleanup; + + if (pkt_out->size != sizeof(obu_with_escape_pattern)) { + fprintf(stderr, "Unescape test: size mismatch, expected %zu got %d\n", + sizeof(obu_with_escape_pattern), pkt_out->size); + ret = -1; + goto cleanup; + } + + if (memcmp(pkt_out->data, obu_with_escape_pattern, sizeof(obu_with_escape_pattern)) != 0) { + fprintf(stderr, "Unescape test: data mismatch\n"); + ret = -1; + goto cleanup; + } + + printf(" Escape removal: PASSED\n"); + ret = 0; + +cleanup: + av_packet_free(&pkt_in); + av_packet_free(&pkt_out); + av_bsf_free(&bsf_ctx); + return ret; +} + +static int test_roundtrip(void) +{ + const AVBitStreamFilter *filter; + AVBSFContext *to_ts_ctx = NULL, *from_ts_ctx = NULL; + AVPacket *pkt_orig = NULL; + AVPacket *pkt_ts = NULL; + AVPacket *pkt_back = NULL; + int ret; + + printf("Testing roundtrip conversion...\n"); + + filter = av_bsf_get_by_name("av1_ts"); + if (!filter) + return -1; + + /* Setup to_ts BSF */ + ret = av_bsf_alloc(filter, &to_ts_ctx); + if (ret < 0) + return ret; + av_opt_set(to_ts_ctx->priv_data, "mode", "to_ts", 0); + to_ts_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(to_ts_ctx); + if (ret < 0) + goto cleanup; + + /* Setup from_ts BSF */ + ret = av_bsf_alloc(filter, &from_ts_ctx); + if (ret < 0) + goto cleanup; + av_opt_set(from_ts_ctx->priv_data, "mode", "from_ts", 0); + from_ts_ctx->par_in->codec_id = AV_CODEC_ID_AV1; + ret = av_bsf_init(from_ts_ctx); + if (ret < 0) + goto cleanup; + + pkt_orig = av_packet_alloc(); + pkt_ts = av_packet_alloc(); + pkt_back = av_packet_alloc(); + if (!pkt_orig || !pkt_ts || !pkt_back) { + ret = AVERROR(ENOMEM); + goto cleanup; + } + + /* Start with Low Overhead OBU containing escape pattern */ + ret = av_new_packet(pkt_orig, sizeof(obu_with_escape_pattern)); + if (ret < 0) + goto cleanup; + memcpy(pkt_orig->data, obu_with_escape_pattern, sizeof(obu_with_escape_pattern)); + + /* Convert to TS format */ + ret = av_bsf_send_packet(to_ts_ctx, pkt_orig); + if (ret < 0) + goto cleanup; + ret = av_bsf_receive_packet(to_ts_ctx, pkt_ts); + if (ret < 0) + goto cleanup; + + /* Convert back from TS format */ + ret = av_bsf_send_packet(from_ts_ctx, pkt_ts); + if (ret < 0) + goto cleanup; + ret = av_bsf_receive_packet(from_ts_ctx, pkt_back); + if (ret < 0) + goto cleanup; + + /* Verify roundtrip produces identical data */ + if (pkt_back->size != sizeof(obu_with_escape_pattern)) { + fprintf(stderr, "Roundtrip: size mismatch\n"); + ret = -1; + goto cleanup; + } + + if (memcmp(pkt_back->data, obu_with_escape_pattern, sizeof(obu_with_escape_pattern)) != 0) { + fprintf(stderr, "Roundtrip: data mismatch\n"); + ret = -1; + goto cleanup; + } + + printf(" Roundtrip: PASSED\n"); + ret = 0; + +cleanup: + av_packet_free(&pkt_orig); + av_packet_free(&pkt_ts); + av_packet_free(&pkt_back); + av_bsf_free(&to_ts_ctx); + av_bsf_free(&from_ts_ctx); + return ret; +} + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + int failed = 0; + + printf("=== AV1 TS BSF Memory Test ===\n\n"); + + if (test_to_ts_mode() < 0) + failed++; + + if (test_from_ts_mode() < 0) + failed++; + + if (test_escape_handling() < 0) + failed++; + + if (test_roundtrip() < 0) + failed++; + + printf("\n=== Results: %d tests, %d failed ===\n", + 4, failed); + + return failed > 0 ? 1 : 0; +} diff --git a/tests/fate/api.mak b/tests/fate/api.mak index b760de5eb6..902ed972ab 100644 --- a/tests/fate/api.mak +++ b/tests/fate/api.mak @@ -28,6 +28,11 @@ fate-api-threadmessage: $(APITESTSDIR)/api-threadmessage-test$(EXESUF) fate-api-threadmessage: CMD = run $(APITESTSDIR)/api-threadmessage-test$(EXESUF) 3 10 30 50 2 20 40 fate-api-threadmessage: CMP = null +FATE_API-$(CONFIG_AVCODEC) += fate-api-av1-ts-bsf +fate-api-av1-ts-bsf: $(APITESTSDIR)/api-av1-ts-bsf-test$(EXESUF) +fate-api-av1-ts-bsf: CMD = run $(APITESTSDIR)/api-av1-ts-bsf-test$(EXESUF) +fate-api-av1-ts-bsf: CMP = null + FATE_API_SAMPLES-$(CONFIG_AVFORMAT) += $(FATE_API_SAMPLES_LIBAVFORMAT-yes) ifdef SAMPLES -- 2.49.1 From 7c2f750215bf57b32393fb7ff2ff01355534e4ff Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Sat, 27 Dec 2025 17:27:24 +0800 Subject: [PATCH 11/11] tests/fate/lavf-container: add AV1 MPEG-TS muxing test --- tests/fate/lavf-container.mak | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fate/lavf-container.mak b/tests/fate/lavf-container.mak index d1007a428a..14bd9825b6 100644 --- a/tests/fate/lavf-container.mak +++ b/tests/fate/lavf-container.mak @@ -74,6 +74,7 @@ fate-lavf-container fate-lavf: $(FATE_LAVF_CONTAINER) FATE_LAVF_CONTAINER_FATE-$(call CRC, APV MOV,, APV_PARSER MP4_MUXER) += apv.mp4 FATE_LAVF_CONTAINER_FATE-$(call CRC, IVF MOV, AV1, AV1_PARSER EXTRACT_EXTRADATA_BSF MP4_MUXER) += av1.mp4 FATE_LAVF_CONTAINER_FATE-$(call CRC, IVF MATROSKA, AV1, AV1_PARSER EXTRACT_EXTRADATA_BSF MATROSKA_MUXER) += av1.mkv +FATE_LAVF_CONTAINER_FATE-$(call CRC, IVF MPEGTS, AV1, AV1_PARSER AV1_TS_BSF MPEGTS_MUXER) += av1.ts FATE_LAVF_CONTAINER_FATE-$(call CRC, MOV EVC,, EVC_PARSER MP4_MUXER) += evc.mp4 FATE_LAVF_CONTAINER_FATE-$(call CRC, MOV H264,, H264_PARSER EXTRACT_EXTRADATA_BSF MP4_MUXER) += h264.mp4 FATE_LAVF_CONTAINER_FATE-$(call CRC, MOV HEVC,, HEVC_PARSER EXTRACT_EXTRADATA_BSF MP4_MUXER) += hevc.mp4 @@ -96,6 +97,7 @@ $(FATE_LAVF_CONTAINER_FATE): $(AREF) $(VREF) fate-lavf-fate-apv.mp4: CMD = lavf_container_fate "apv/profile_422-10.apv" "" "" "-c:v copy" fate-lavf-fate-av1.mp4: CMD = lavf_container_fate "av1-test-vectors/av1-1-b8-05-mv.ivf" "-c:v av1" "" "-c:v copy" fate-lavf-fate-av1.mkv: CMD = lavf_container_fate "av1-test-vectors/av1-1-b8-05-mv.ivf" "-c:v av1" "" "-c:v copy" +fate-lavf-fate-av1.ts: CMD = lavf_container_fate "av1-test-vectors/av1-1-b8-05-mv.ivf" "-c:v av1" "" "-c:v copy" fate-lavf-fate-evc.mp4: CMD = lavf_container_fate "evc/akiyo_cif.evc" "" "" "-c:v copy" fate-lavf-fate-h264.mp4: CMD = lavf_container_fate "h264/intra_refresh.h264" "" "" "-c:v copy" fate-lavf-fate-hevc.mp4: CMD = lavf_container_fate "hevc-conformance/HRD_A_Fujitsu_2.bit" "" "" "-c:v copy" -- 2.49.1 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
