PR #23609 opened by Zhao Zhili (quink) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23609 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23609.patch
The format stores video-only H.264 in a fixed header + index table + chunk layout. Each chunk is a self-contained GOP, enabling simple random access via the index. Fix #23540 >From 59e2023c540738d54a05c65205a5b7b8a53421b2 Mon Sep 17 00:00:00 2001 From: Zhao Zhili <[email protected]> Date: Wed, 24 Jun 2026 20:32:07 +0800 Subject: [PATCH] avformat/mvrdec: add MVR CCTV demuxer The format stores video-only H.264 in a fixed header + index table + chunk layout. Each chunk is a self-contained GOP, enabling simple random access via the index. Fix #23540 --- Changelog | 1 + libavformat/Makefile | 1 + libavformat/allformats.c | 1 + libavformat/mvrdec.c | 195 +++++++++++++++++++++++++++++++++++++++ libavformat/version.h | 2 +- 5 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 libavformat/mvrdec.c diff --git a/Changelog b/Changelog index 8cfac4c1a7..edc78c0b60 100644 --- a/Changelog +++ b/Changelog @@ -19,6 +19,7 @@ version <next>: - Bitstream filter to split Dolby Vision multi-layer HEVC - Add AMF hardware memory mapping support. - ONNX Runtime DNN backend with GPU execution provider support +- MVR demuxer version 8.1: diff --git a/libavformat/Makefile b/libavformat/Makefile index 752436cf5f..395e12ad3a 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -422,6 +422,7 @@ OBJS-$(CONFIG_MTV_DEMUXER) += mtv.o OBJS-$(CONFIG_MUSX_DEMUXER) += musx.o OBJS-$(CONFIG_MV_DEMUXER) += mvdec.o OBJS-$(CONFIG_MVI_DEMUXER) += mvi.o +OBJS-$(CONFIG_MVR_DEMUXER) += mvrdec.o OBJS-$(CONFIG_MXF_DEMUXER) += mxfdec.o mxf.o avlanguage.o OBJS-$(CONFIG_MXF_MUXER) += mxfenc.o mxf.o OBJS-$(CONFIG_MXG_DEMUXER) += mxg.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index af7eea5e5c..0d6463623c 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -319,6 +319,7 @@ extern const FFInputFormat ff_mtv_demuxer; extern const FFInputFormat ff_musx_demuxer; extern const FFInputFormat ff_mv_demuxer; extern const FFInputFormat ff_mvi_demuxer; +extern const FFInputFormat ff_mvr_demuxer; extern const FFInputFormat ff_mxf_demuxer; extern const FFOutputFormat ff_mxf_muxer; extern const FFOutputFormat ff_mxf_d10_muxer; diff --git a/libavformat/mvrdec.c b/libavformat/mvrdec.c new file mode 100644 index 0000000000..ae28778f7d --- /dev/null +++ b/libavformat/mvrdec.c @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026 Zhao Zhili + * + * 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 <limits.h> + +#include "libavutil/intreadwrite.h" +#include "libavutil/mem.h" +#include "avformat.h" +#include "demux.h" +#include "internal.h" + +/* + * Layout: [512 B header][index: N x 8 B (start_ms:u32, offset:u32)] + * [chunks: 16 B local header + H.264 Annex-B NALs] + * Each chunk is a self-contained GOP (SPS+PPS+IDR + P-slices). + */ +#define MVR_HEADER_SIZE 512 +#define MVR_ENTRY_SIZE 8 +#define MVR_CHUNK_HEADER 16 + +typedef struct MVRContext { + /* index of the chunk to read next */ + unsigned next_chunk; +} MVRContext; + +static int mvr_probe(const AVProbeData *p) +{ + unsigned width, height; + unsigned index_table_size, index_capacity, index_count; + size_t index_size; + + if (p->buf_size < 0x40) + return 0; + + if (AV_RL32(p->buf) != 4) + return 0; + + width = AV_RL32(p->buf + 0x04); + height = AV_RL32(p->buf + 0x08); + if (width == 0 || width > 8192 || height == 0 || height > 8192) + return 0; + + index_table_size = AV_RL32(p->buf + 0x34); + index_capacity = AV_RL32(p->buf + 0x38); + index_count = AV_RL32(p->buf + 0x3c); + + if (index_capacity == 0 || + av_size_mult(index_capacity, MVR_ENTRY_SIZE, &index_size) < 0 || + index_table_size != index_size || + index_count > index_capacity) + return 0; + + return AVPROBE_SCORE_EXTENSION; +} + +static int mvr_read_header(AVFormatContext *s) +{ + AVIOContext *pb = s->pb; + AVStream *st; + int64_t filesize; + uint64_t time_meta_ms; + uint32_t index_count, start_ms, offset; + int ret; + + filesize = avio_size(pb); + if (filesize < MVR_HEADER_SIZE + MVR_ENTRY_SIZE) + return AVERROR_INVALIDDATA; + + st = avformat_new_stream(s, NULL); + if (!st) + return AVERROR(ENOMEM); + + st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + st->codecpar->codec_id = AV_CODEC_ID_H264; + ffstream(st)->need_parsing = AVSTREAM_PARSE_FULL; + avpriv_set_pts_info(st, 64, 1, 1000); + + avio_skip(pb, 4); + st->codecpar->width = avio_rl32(pb); + st->codecpar->height = avio_rl32(pb); + + avio_skip(pb, 0x28 - 0x0c); + time_meta_ms = avio_rl64(pb); + ff_dict_set_timestamp(&s->metadata, "creation_time", + time_meta_ms * 1000); + st->duration = avio_rl32(pb); + + /* skip index_table_size and index_capacity */ + avio_skip(pb, 8); + index_count = avio_rl32(pb); + + if (avio_seek(pb, MVR_HEADER_SIZE, SEEK_SET) < 0) + return AVERROR_INVALIDDATA; + + /* Build index from (start_ms, offset) pairs. Each chunk size is + * derived from the next entry's offset, so we look one entry ahead. */ + start_ms = avio_rl32(pb); + offset = avio_rl32(pb); + for (unsigned i = 0; i < index_count; i++) { + uint32_t next_start_ms; + int64_t next_offset, chunk_size; + + if (i + 1 < index_count) { + next_start_ms = avio_rl32(pb); + next_offset = avio_rl32(pb); + } else { + next_start_ms = 0; + next_offset = filesize; + } + + chunk_size = next_offset - offset; + if (chunk_size <= MVR_CHUNK_HEADER || chunk_size > INT_MAX) + return AVERROR_INVALIDDATA; + + ret = av_add_index_entry(st, offset, start_ms, + chunk_size, 0, AVINDEX_KEYFRAME); + if (ret < 0) + return ret; + + start_ms = next_start_ms; + offset = next_offset; + } + + return 0; +} + +static int mvr_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + MVRContext *mvr = s->priv_data; + AVStream *st = s->streams[0]; + FFStream *const sti = ffstream(st); + AVIndexEntry *e; + int64_t h264_pos; + int h264_size, ret; + + /* Resync after a generic seek: locate the index entry the framework + * positioned avio to via its updated cur_dts. */ + if (s->io_repositioned) { + int idx = av_index_search_timestamp(st, sti->cur_dts, + AVSEEK_FLAG_BACKWARD); + if (idx < 0) + return AVERROR(EINVAL); + mvr->next_chunk = idx; + s->io_repositioned = 0; + } + + if (mvr->next_chunk >= sti->nb_index_entries) + return AVERROR_EOF; + + e = &sti->index_entries[mvr->next_chunk]; + h264_pos = e->pos + MVR_CHUNK_HEADER; + h264_size = e->size - MVR_CHUNK_HEADER; + + if (avio_seek(s->pb, h264_pos, SEEK_SET) < 0) + return AVERROR_EOF; + + ret = av_get_packet(s->pb, pkt, h264_size); + if (ret < 0) + return ret; + + pkt->pts = e->timestamp; + pkt->dts = AV_NOPTS_VALUE; + pkt->pos = e->pos; + mvr->next_chunk++; + + return 0; +} + +const FFInputFormat ff_mvr_demuxer = { + .p.name = "mvr", + .p.long_name = NULL_IF_CONFIG_SMALL("MVR CCTV"), + .p.extensions = "mvr", + .p.flags = AVFMT_GENERIC_INDEX, + .priv_data_size = sizeof(MVRContext), + .read_probe = mvr_probe, + .read_header = mvr_read_header, + .read_packet = mvr_read_packet, +}; diff --git a/libavformat/version.h b/libavformat/version.h index 752aac16f7..a7c80dc564 100644 --- a/libavformat/version.h +++ b/libavformat/version.h @@ -31,7 +31,7 @@ #include "version_major.h" -#define LIBAVFORMAT_VERSION_MINOR 0 +#define LIBAVFORMAT_VERSION_MINOR 1 #define LIBAVFORMAT_VERSION_MICRO 100 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
