[PR] avformat/mvrdec: add MVR CCTV demuxer (PR #23609)

Zhao Zhili via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178250123118.59.11830793513460046241@29965ddac10e>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.