PR #22470 opened by James Almer (jamrial)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22470
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22470.patch


>From 3c82ebf0a4f0e29a53c723d5966430f225626b6e Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Mon, 16 Feb 2026 12:03:04 -0300
Subject: [PATCH 1/4] avformat/options: add missing AVOption for
 AVStreamGroupLCEVC

Signed-off-by: James Almer <[email protected]>
---
 libavformat/options.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libavformat/options.c b/libavformat/options.c
index d08013bfd4..09f104c3a9 100644
--- a/libavformat/options.c
+++ b/libavformat/options.c
@@ -351,6 +351,8 @@ static const AVClass tile_grid_class = {
 
 #define OFFSET(x) offsetof(AVStreamGroupLCEVC, x)
 static const AVOption lcevc_options[] = {
+    { "lcevc_index", "size of video after LCEVC enhancement has been applied", 
OFFSET(lcevc_index),
+        AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, FLAGS },
     { "video_size", "size of video after LCEVC enhancement has been applied", 
OFFSET(width),
         AV_OPT_TYPE_IMAGE_SIZE, { .str = NULL }, 0, INT_MAX, FLAGS },
     { NULL },
-- 
2.52.0


>From 26f6dcfb0e5a0c4ad5e13e9c6d04780ad405d87e Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Mon, 9 Mar 2026 20:20:53 -0300
Subject: [PATCH 2/4] avcodec/lcevc_parser: move the resolution type table to a
 header

Will be useful in the following commit.

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/lcevc_parse.h  | 19 +++++++++++++++++++
 libavcodec/lcevc_parser.c | 19 -------------------
 2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/libavcodec/lcevc_parse.h b/libavcodec/lcevc_parse.h
index f56758a1a5..cd9cd212b2 100644
--- a/libavcodec/lcevc_parse.h
+++ b/libavcodec/lcevc_parse.h
@@ -23,6 +23,25 @@
 
 #include "get_bits.h"
 
+static const struct {
+    int width;
+    int height;
+} resolution_type_lut[63] = {
+    { 0, 0},
+    { 360,  200 },  { 400,  240 },  { 480,  320 },  { 640,  360 },
+    { 640,  480 },  { 768,  480 },  { 800,  600 },  { 852,  480 },
+    { 854,  480 },  { 856,  480 },  { 960,  540 },  { 960,  640 },
+    { 1024, 576 },  { 1024, 600 },  { 1024, 768 },  { 1152, 864 },
+    { 1280, 720 },  { 1280, 800 },  { 1280, 1024 }, { 1360, 768 },
+    { 1366, 768 },  { 1920, 1200 }, { 2048, 1080 }, { 2048, 1152 },
+    { 2048, 1536 }, { 2160, 1440 }, { 2560, 1440 }, { 2560, 1600 },
+    { 2560, 2048 }, { 3200, 1800 }, { 3200, 2048 }, { 3200, 2400 },
+    { 3440, 1440 }, { 3840, 1600 }, { 3840, 2160 }, { 3840, 2400 },
+    { 4096, 2160 }, { 4096, 3072 }, { 5120, 2880 }, { 5120, 3200 },
+    { 5120, 4096 }, { 6400, 4096 }, { 6400, 4800 }, { 7680, 4320 },
+    { 7680, 4800 },
+};
+
 static inline uint64_t get_mb(GetBitContext *s) {
     int more, i = 0;
     uint64_t mb = 0;
diff --git a/libavcodec/lcevc_parser.c b/libavcodec/lcevc_parser.c
index 1de44a8bda..766d98e26e 100644
--- a/libavcodec/lcevc_parser.c
+++ b/libavcodec/lcevc_parser.c
@@ -82,25 +82,6 @@ static const enum AVPixelFormat pix_fmts[4][4] = {
       AV_PIX_FMT_YUV422P14, AV_PIX_FMT_YUV444P14, },
 };
 
-static const struct {
-    int width;
-    int height;
-} resolution_type_lut[63] = {
-    { 0, 0},
-    { 360,  200 },  { 400,  240 },  { 480,  320 },  { 640,  360 },
-    { 640,  480 },  { 768,  480 },  { 800,  600 },  { 852,  480 },
-    { 854,  480 },  { 856,  480 },  { 960,  540 },  { 960,  640 },
-    { 1024, 576 },  { 1024, 600 },  { 1024, 768 },  { 1152, 864 },
-    { 1280, 720 },  { 1280, 800 },  { 1280, 1024 }, { 1360, 768 },
-    { 1366, 768 },  { 1920, 1200 }, { 2048, 1080 }, { 2048, 1152 },
-    { 2048, 1536 }, { 2160, 1440 }, { 2560, 1440 }, { 2560, 1600 },
-    { 2560, 2048 }, { 3200, 1800 }, { 3200, 2048 }, { 3200, 2400 },
-    { 3440, 1440 }, { 3840, 1600 }, { 3840, 2160 }, { 3840, 2400 },
-    { 4096, 2160 }, { 4096, 3072 }, { 5120, 2880 }, { 5120, 3200 },
-    { 5120, 4096 }, { 6400, 4096 }, { 6400, 4800 }, { 7680, 4320 },
-    { 7680, 4800 },
-};
-
 static int parse_nal_unit(AVCodecParserContext *s, AVCodecContext *avctx,
                           const H2645NAL *nal)
 {
-- 
2.52.0


>From d9a2f38c5dcc37999f2c4786b05d41393ef4cab9 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Mon, 16 Feb 2026 12:02:14 -0300
Subject: [PATCH 3/4] avformat/movenc: add support for LCEVC track muxing

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/Makefile       |   1 +
 libavformat/Makefile      |   4 +-
 libavformat/h2645_parse.c |  19 +++
 libavformat/lcevc.c       | 278 ++++++++++++++++++++++++++++++++++++++
 libavformat/lcevc.h       |  29 ++++
 libavformat/movenc.c      |  54 +++++++-
 6 files changed, 380 insertions(+), 5 deletions(-)
 create mode 100644 libavformat/h2645_parse.c
 create mode 100644 libavformat/lcevc.c
 create mode 100644 libavformat/lcevc.h

diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 12a8265025..b7afb6a5ce 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1129,6 +1129,7 @@ OBJS-$(CONFIG_TAK_DEMUXER)             += tak.o
 # libavformat dependencies for static builds
 STLIBOBJS-$(CONFIG_AVFORMAT)           += to_upper4.o
 STLIBOBJS-$(CONFIG_ISO_MEDIA)          += mpegaudiotabs.o
+STLIBOBJS-$(CONFIG_ISO_WRITER)         += h2645_parse.o
 STLIBOBJS-$(CONFIG_FLV_MUXER)          += mpeg4audio_sample_rates.o
 STLIBOBJS-$(CONFIG_HLS_DEMUXER)        += ac3_channel_layout_tab.o
 STLIBOBJS-$(CONFIG_IMAGE_JPEGXL_PIPE_DEMUXER) += jpegxl_parse.o
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 4786a9345a..4ffefb83cc 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -40,7 +40,7 @@ OBJS-$(HAVE_LIBC_MSVCRT)                 += file_open.o
 
 # subsystems
 OBJS-$(CONFIG_ISO_MEDIA)                 += isom.o
-OBJS-$(CONFIG_ISO_WRITER)                += avc.o hevc.o vvc.o
+OBJS-$(CONFIG_ISO_WRITER)                += avc.o hevc.o vvc.o lcevc.o
 OBJS-$(CONFIG_IAMFDEC)                   += iamf_reader.o iamf_parse.o iamf.o
 OBJS-$(CONFIG_IAMFENC)                   += iamf_writer.o iamf.o
 OBJS-$(CONFIG_NETWORK)                   += network.o
@@ -742,7 +742,7 @@ OBJS-$(CONFIG_LIBZMQ_PROTOCOL)           += libzmq.o
 # Objects duplicated from other libraries for shared builds
 SHLIBOBJS                                += log2_tab.o to_upper4.o
 SHLIBOBJS-$(CONFIG_ISO_MEDIA)            += mpegaudiotabs.o
-SHLIBOBJS-$(CONFIG_ISO_WRITER)           += golomb_tab.o
+SHLIBOBJS-$(CONFIG_ISO_WRITER)           += golomb_tab.o h2645_parse.o
 SHLIBOBJS-$(CONFIG_FLV_MUXER)            += mpeg4audio_sample_rates.o
 SHLIBOBJS-$(CONFIG_HLS_DEMUXER)          += ac3_channel_layout_tab.o
 SHLIBOBJS-$(CONFIG_IMAGE_JPEGXL_PIPE_DEMUXER)    += jpegxl_parse.o
diff --git a/libavformat/h2645_parse.c b/libavformat/h2645_parse.c
new file mode 100644
index 0000000000..5a6ce628e1
--- /dev/null
+++ b/libavformat/h2645_parse.c
@@ -0,0 +1,19 @@
+/*
+ * 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 "libavcodec/h2645_parse.c"
diff --git a/libavformat/lcevc.c b/libavformat/lcevc.c
new file mode 100644
index 0000000000..9f2e61e8b7
--- /dev/null
+++ b/libavformat/lcevc.c
@@ -0,0 +1,278 @@
+/*
+ * LCEVC helper functions for muxers
+ *
+ * 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 "libavutil/error.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/mem.h"
+#include "libavcodec/bytestream.h"
+#include "libavcodec/h2645_parse.h"
+#include "libavcodec/lcevc.h"
+#include "libavcodec/lcevc_parse.h"
+#include "libavcodec/leb.h"
+#include "avio.h"
+#include "avio_internal.h"
+#include "lcevc.h"
+#include "nal.h"
+
+typedef struct LCEVCDecoderConfigurationRecord {
+    uint8_t  profile_idc;
+    uint8_t  level_idc;
+    uint8_t  chroma_format_idc;
+    uint8_t  bit_depth_luma_minus8;
+    uint8_t  bit_depth_chroma_minus8;
+    uint32_t pic_width_in_luma_samples;
+    uint32_t pic_height_in_luma_samples;
+} LCEVCDecoderConfigurationRecord;
+
+/**
+ * Rewrite the NALu stripping the unnedded blocks.
+ * Given that length fields coded inside the NALu are not aware of any 
emulation_3bytes
+ * present in the bitstream, we need to keep track of the raw buffer as we 
navigate
+ * the stripped buffer in order to write proper NALu sizes.
+ */
+static int write_nalu(LCEVCDecoderConfigurationRecord *lvcc, AVIOContext *pb,
+                      const H2645NAL *nal)
+{
+    GetByteContext gbc, raw_gbc;
+    int64_t start = avio_tell(pb), end;
+    int sc = 0, gc = 0;
+    int skipped_byte_pos = 0, nalu_length = 3;
+
+    bytestream2_init(&gbc, nal->data, nal->size);
+    bytestream2_init(&raw_gbc, nal->raw_data, nal->raw_size);
+    avio_wb16(pb, 0); // size placeholder
+    avio_wb16(pb, bytestream2_get_be16(&gbc)); // nal_unit_header
+    bytestream2_skip(&raw_gbc, 2);
+
+    while (bytestream2_get_bytes_left(&gbc) > 1 && (!sc || !gc)) {
+        GetBitContext gb;
+        uint64_t payload_size;
+        int payload_size_type, payload_type;
+        int block_size, raw_block_size, block_end;
+
+        init_get_bits8(&gb, gbc.buffer, bytestream2_get_bytes_left(&gbc));
+
+        payload_size_type = get_bits(&gb, 3);
+        payload_type      = get_bits(&gb, 5);
+        payload_size      = payload_size_type;
+        if (payload_size_type == 6)
+            return AVERROR_PATCHWELCOME;
+        if (payload_size_type == 7)
+            payload_size = get_mb(&gb);
+
+        if (payload_size > INT_MAX - (get_bits_count(&gb) >> 3))
+            return AVERROR_INVALIDDATA;
+
+        block_size = raw_block_size = payload_size + (get_bits_count(&gb) >> 
3);
+        if (block_size >= bytestream2_get_bytes_left(&gbc))
+            return AVERROR_INVALIDDATA;
+
+        block_end = bytestream2_tell(&gbc) + block_size;
+        // Take into account removed emulation 3bytes, as payload_size in
+        // the bitstream is not aware of them.
+        for (; skipped_byte_pos < nal->skipped_bytes; skipped_byte_pos++) {
+            if (nal->skipped_bytes_pos[skipped_byte_pos] >= block_end)
+                break;
+            raw_block_size++;
+        }
+
+        switch (payload_type) {
+        case 0:
+            if (sc)
+                break;
+
+            lvcc->profile_idc = get_bits(&gb, 4);
+            lvcc->level_idc = get_bits(&gb, 4);
+
+            avio_write(pb, raw_gbc.buffer, raw_block_size);
+            nalu_length += raw_block_size;
+            sc = 1;
+            break;
+        case 1: {
+            int resolution_type, bit_depth;
+            int processed_planes_type_flag;
+
+            if (gc)
+                break;
+
+            processed_planes_type_flag = get_bits1(&gb);
+            resolution_type = get_bits(&gb, 6);
+
+            skip_bits1(&gb);
+            lvcc->chroma_format_idc = get_bits(&gb, 2);
+
+            skip_bits(&gb, 2);
+            bit_depth = get_bits(&gb, 2) * 2; // enhancement_depth_type
+            lvcc->bit_depth_luma_minus8 = bit_depth;
+            lvcc->bit_depth_chroma_minus8 = bit_depth;
+
+            if (resolution_type < 63) {
+                lvcc->pic_width_in_luma_samples  = 
resolution_type_lut[resolution_type].width;
+                lvcc->pic_height_in_luma_samples = 
resolution_type_lut[resolution_type].height;
+            } else {
+                int upsample_type, tile_dimensions_type;
+                int temporal_step_width_modifier_signalled_flag, 
level1_filtering_signalled_flag;
+                // Skip syntax elements until we get to the custom dimension 
ones
+                temporal_step_width_modifier_signalled_flag = get_bits1(&gb);
+                skip_bits(&gb, 3);
+                upsample_type = get_bits(&gb, 3);
+                level1_filtering_signalled_flag = get_bits1(&gb);
+                skip_bits(&gb, 4);
+                tile_dimensions_type = get_bits(&gb, 2);
+                skip_bits(&gb, 4);
+                if (processed_planes_type_flag)
+                    skip_bits(&gb, 4);
+                if (temporal_step_width_modifier_signalled_flag)
+                    skip_bits(&gb, 8);
+                if (upsample_type)
+                    skip_bits_long(&gb, 64);
+                if (level1_filtering_signalled_flag)
+                    skip_bits(&gb, 8);
+                if (tile_dimensions_type) {
+                    if (tile_dimensions_type == 3)
+                        skip_bits_long(&gb, 32);
+                    skip_bits(&gb, 8);
+                }
+
+                lvcc->pic_width_in_luma_samples = get_bits(&gb, 16);
+                lvcc->pic_height_in_luma_samples = get_bits(&gb, 16);
+            }
+
+            if (!lvcc->pic_width_in_luma_samples || 
!lvcc->pic_height_in_luma_samples)
+                break;
+
+            avio_write(pb, raw_gbc.buffer, raw_block_size);
+            nalu_length += raw_block_size;
+            gc = 1;
+            break;
+        }
+        case 5:
+            avio_write(pb, raw_gbc.buffer, raw_block_size);
+            nalu_length += raw_block_size;
+            break;
+        default:
+            break;
+        }
+
+        bytestream2_skip(&gbc, block_size);
+        bytestream2_skip(&raw_gbc, raw_block_size);
+    }
+
+    if (!sc || !gc)
+        return AVERROR_INVALIDDATA;
+
+    avio_w8(pb, 0x80); // rbsp_alignment bits
+
+    end = avio_tell(pb);
+    avio_seek(pb, start, SEEK_SET);
+    avio_wb16(pb, nalu_length);
+    avio_seek(pb, end, SEEK_SET);
+
+    return 0;
+}
+
+int ff_isom_write_lvcc(AVIOContext *pb, const uint8_t *data, int len)
+{
+    LCEVCDecoderConfigurationRecord lvcc = { 0 };
+    AVIOContext *idr_pb = NULL, *nidr_pb = NULL;
+    H2645Packet h2645_pkt = { 0 };
+    uint8_t *idr, *nidr;
+    uint32_t idr_size = 0, nidr_size = 0;
+    int ret, nb_idr = 0, nb_nidr = 0;
+
+    if (len <= 6)
+        return AVERROR_INVALIDDATA;
+
+    /* check for start code */
+    if (AV_RB32(data) != 1) {
+        avio_write(pb, data, len);
+        return 0;
+    }
+
+    ret = ff_h2645_packet_split(&h2645_pkt, data, len, NULL, 0, 
AV_CODEC_ID_LCEVC, 0);
+    if (ret < 0)
+        return ret;
+
+    ret = avio_open_dyn_buf(&idr_pb);
+    if (ret < 0)
+        goto fail;
+    ret = avio_open_dyn_buf(&nidr_pb);
+    if (ret < 0)
+        goto fail;
+
+    /* look for IDR or NON_IDR */
+    for (int i = 0; i < h2645_pkt.nb_nals; i++) {
+        const H2645NAL *nal = &h2645_pkt.nals[i];
+
+        if (nal->type == LCEVC_IDR_NUT) {
+            nb_idr++;
+
+            ret = write_nalu(&lvcc, idr_pb, nal);
+            if (ret < 0)
+                return ret;
+        } else if (nal->type == LCEVC_NON_IDR_NUT) {
+            nb_nidr++;
+
+            ret = write_nalu(&lvcc, nidr_pb, nal);
+            if (ret < 0)
+                return ret;
+        }
+    }
+    idr_size = avio_get_dyn_buf(idr_pb, &idr);
+    nidr_size = avio_get_dyn_buf(nidr_pb, &nidr);
+
+    if (!idr_size && !nidr_size) {
+        ret = AVERROR_INVALIDDATA;
+        goto fail;
+    }
+
+    avio_w8(pb, 1); /* version */
+    avio_w8(pb, lvcc.profile_idc);
+    avio_w8(pb, lvcc.level_idc);
+    avio_w8(pb, (lvcc.chroma_format_idc     << 6) |
+                (lvcc.bit_depth_luma_minus8 << 3) |
+                 lvcc.bit_depth_chroma_minus8);
+    avio_w8(pb, 0xff); /* 2 bits nal size length - 1 (11) + 6 bits reserved 
(111111)*/
+    avio_wb32(pb, lvcc.pic_width_in_luma_samples);
+    avio_wb32(pb, lvcc.pic_height_in_luma_samples);
+    avio_w8(pb, 0xff);
+
+    int nb_arrays = !!nb_idr + !!nb_nidr;
+    avio_w8(pb, nb_arrays);
+
+    if (nb_idr) {
+        avio_w8(pb, LCEVC_IDR_NUT);
+        avio_wb16(pb, nb_idr);
+        avio_write(pb, idr, idr_size);
+    }
+    if (nb_nidr) {
+        avio_w8(pb, LCEVC_NON_IDR_NUT);
+        avio_wb16(pb, nb_idr);
+        avio_write(pb, nidr, nidr_size);
+    }
+
+    ret = 0;
+fail:
+    ffio_free_dyn_buf(&idr_pb);
+    ffio_free_dyn_buf(&nidr_pb);
+    ff_h2645_packet_uninit(&h2645_pkt);
+
+    return ret;
+}
diff --git a/libavformat/lcevc.h b/libavformat/lcevc.h
new file mode 100644
index 0000000000..76093c4a36
--- /dev/null
+++ b/libavformat/lcevc.h
@@ -0,0 +1,29 @@
+/*
+ * LCEVC helper functions for muxers
+ *
+ * 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
+ */
+
+#ifndef AVFORMAT_LCEVC_H
+#define AVFORMAT_LCEVC_H
+
+#include <stdint.h>
+#include "avio.h"
+
+int ff_isom_write_lvcc(AVIOContext *pb, const uint8_t *data, int len);
+
+#endif /* AVFORMAT_LCEVC_H */
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index fe6b259561..b78a02afef 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -38,6 +38,7 @@
 #include "avc.h"
 #include "evc.h"
 #include "apv.h"
+#include "lcevc.h"
 #include "libavcodec/ac3_parser_internal.h"
 #include "libavcodec/dnxhddata.h"
 #include "libavcodec/flac.h"
@@ -1681,6 +1682,19 @@ static int mov_write_evcc_tag(AVIOContext *pb, MOVTrack 
*track)
     return update_size(pb, pos);
 }
 
+static int mov_write_lvcc_tag(AVIOContext *pb, MOVTrack *track)
+{
+    int64_t pos = avio_tell(pb);
+
+    avio_wb32(pb, 0);
+    ffio_wfourcc(pb, "lvcC");
+
+    ff_isom_write_lvcc(pb, track->extradata[track->last_stsd_index],
+                       track->extradata_size[track->last_stsd_index]);
+
+    return update_size(pb, pos);
+}
+
 static int mov_write_vvcc_tag(AVIOContext *pb, MOVTrack *track)
 {
     int64_t pos = avio_tell(pb);
@@ -2880,6 +2894,8 @@ static int mov_write_video_tag(AVFormatContext *s, 
AVIOContext *pb, MOVMuxContex
     }
     else if (track->par->codec_id ==AV_CODEC_ID_EVC) {
         mov_write_evcc_tag(pb, track);
+    } else if (track->par->codec_id == AV_CODEC_ID_LCEVC) {
+        mov_write_lvcc_tag(pb, track);
     } else if (track->par->codec_id ==AV_CODEC_ID_APV) {
         mov_write_apvc_tag(mov->fc, pb, track);
     } else if (track->par->codec_id == AV_CODEC_ID_VP9) {
@@ -5222,6 +5238,9 @@ static int mov_write_moov_tag(AVIOContext *pb, 
MOVMuxContext *mov,
         if (track->tag == MKTAG('r','t','p',' ')) {
             track->tref_tag = MKTAG('h','i','n','t');
             track->tref_id = mov->tracks[track->src_track].track_id;
+        } else if (track->tag == MKTAG('l','v','c','1')) {
+            track->tref_tag = MKTAG('s','b','a','s');
+            track->tref_id = mov->tracks[track->src_track].track_id;
         } else if (track->par->codec_type == AVMEDIA_TYPE_AUDIO) {
             const AVPacketSideData *sd = 
av_packet_side_data_get(track->st->codecpar->coded_side_data,
                                                                  
track->st->codecpar->nb_coded_side_data,
@@ -6847,6 +6866,7 @@ int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt)
          par->codec_id == AV_CODEC_ID_VVC ||
          par->codec_id == AV_CODEC_ID_VP9 ||
          par->codec_id == AV_CODEC_ID_EVC ||
+         par->codec_id == AV_CODEC_ID_LCEVC ||
          par->codec_id == AV_CODEC_ID_TRUEHD) && !trk->extradata_size[0] &&
          !TAG_IS_AVCI(trk->tag)) {
         /* copy frame to create needed atoms */
@@ -8041,10 +8061,15 @@ static int mov_init(AVFormatContext *s)
         s->streams[0]->disposition |= AV_DISPOSITION_DEFAULT;
     }
 
-#if CONFIG_IAMFENC
     for (i = 0; i < s->nb_stream_groups; i++) {
         AVStreamGroup *stg = s->stream_groups[i];
 
+        if (stg->type == AV_STREAM_GROUP_PARAMS_LCEVC && stg->nb_streams != 2) 
{
+            av_log(s, AV_LOG_ERROR, "Exactly two Streams are needed for Strem 
Groups of type LCEVC\n");
+            return AVERROR(EINVAL);
+        }
+
+#if CONFIG_IAMFENC
         if (stg->type != AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT)
             continue;
 
@@ -8062,8 +8087,8 @@ static int mov_init(AVFormatContext *s)
 
         if (!mov->nb_tracks) // We support one track for the entire IAMF 
structure
             mov->nb_tracks++;
-    }
 #endif
+    }
 
     for (i = 0; i < s->nb_streams; i++) {
         AVStream *st = s->streams[i];
@@ -8378,6 +8403,28 @@ static int mov_init(AVFormatContext *s)
         }
     }
 
+    for (i = 0; i < s->nb_stream_groups; i++) {
+        AVStreamGroup *stg = s->stream_groups[i];
+
+        if (stg->type != AV_STREAM_GROUP_PARAMS_LCEVC)
+            continue;
+
+        AVStreamGroupLCEVC *lcevc = stg->params.lcevc;
+        AVStream *st    = stg->streams[lcevc->lcevc_index];
+        MOVTrack *track = st->priv_data;
+
+        for (i = 0; i < mov->nb_tracks; i++) {
+            MOVTrack *trk = &mov->tracks[i];
+
+            if (trk->st == stg->streams[!lcevc->lcevc_index])
+                break;
+        }
+        track->src_track = i;
+
+        track->par->width = lcevc->width;
+        track->par->height = track->height = lcevc->height;
+    }
+
     enable_tracks(s);
     return 0;
 }
@@ -8530,7 +8577,7 @@ static int mov_write_header(AVFormatContext *s)
             AVStream *st = mov->tracks[i].st;
             t = global_tcr;
 
-            if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
+            if (mov->tracks[i].par->codec_type == AVMEDIA_TYPE_VIDEO) {
                 AVTimecode tc;
                 if (!t)
                     t = av_dict_get(st->metadata, "timecode", NULL, 0);
@@ -8892,6 +8939,7 @@ static const AVCodecTag codec_mp4_tags[] = {
     { AV_CODEC_ID_VVC,             MKTAG('v', 'v', 'c', '1') },
     { AV_CODEC_ID_VVC,             MKTAG('v', 'v', 'i', '1') },
     { AV_CODEC_ID_EVC,             MKTAG('e', 'v', 'c', '1') },
+    { AV_CODEC_ID_LCEVC,           MKTAG('l', 'v', 'c', '1') },
     { AV_CODEC_ID_APV,             MKTAG('a', 'p', 'v', '1') },
     { AV_CODEC_ID_MPEG2VIDEO,      MKTAG('m', 'p', '4', 'v') },
     { AV_CODEC_ID_MPEG1VIDEO,      MKTAG('m', 'p', '4', 'v') },
-- 
2.52.0


>From 229ee9dec1fb88fc843f76b6964a329bf88a1476 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Mon, 16 Feb 2026 12:03:28 -0300
Subject: [PATCH 4/4] fftools/ffmpeg_mux_init: add support for LCEVC Stream
 Group muxing

Signed-off-by: James Almer <[email protected]>
---
 fftools/ffmpeg_mux_init.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index 7e51ce76ad..e0eecf78f2 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -2552,6 +2552,8 @@ static int of_map_group(Muxer *mux, AVDictionary **dict, 
AVBPrint *bp, const cha
         }
         break;
     }
+    case AV_STREAM_GROUP_PARAMS_LCEVC:
+        break;
     default:
         av_log(mux, AV_LOG_ERROR, "Unsupported mapped group type %d.\n", 
stg->type);
         ret = AVERROR(EINVAL);
@@ -2574,6 +2576,8 @@ static int of_parse_group_token(Muxer *mux, const char 
*token, char *ptr)
                 { .i64 = AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT },    .unit 
= "type" },
             { "iamf_mix_presentation", NULL, 0, AV_OPT_TYPE_CONST,
                 { .i64 = AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION }, .unit 
= "type" },
+            { "lcevc", NULL, 0, AV_OPT_TYPE_CONST,
+                { .i64 = AV_STREAM_GROUP_PARAMS_LCEVC }, .unit = "type" },
         { NULL },
     };
     const AVClass class = {
@@ -2672,8 +2676,6 @@ static int of_parse_group_token(Muxer *mux, const char 
*token, char *ptr)
         ret = of_parse_iamf_submixes(mux, stg, ptr);
         break;
     default:
-        av_log(mux, AV_LOG_FATAL, "Unknown group type %d.\n", type);
-        ret = AVERROR(EINVAL);
         break;
     }
 
-- 
2.52.0

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

Reply via email to