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

Given containers can export base and enhancement as separate streams, we need 
to merge both payloads in order for them to be usable by libavcodec and 
libavfilter.


>From fabbd8bd928c1fab89c2460f3326e199dbb7702b Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 6 Mar 2026 10:24:28 -0300
Subject: [PATCH 1/7] avcodec/codec_desc: add a codec prop to signal
 enhancement layers

Some video codecs are not meant to output frames on their own but to be applied 
on
top of frames generated by other codecs, as is the case of LCEVC, Dolby Vision, 
etc.
Add a codec prop to signal this kind of codec, so that library users may know 
to not
expect a standalone decoder for them to be present.

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/allcodecs.c  | 4 ++++
 libavcodec/codec_desc.h | 7 +++++++
 2 files changed, 11 insertions(+)

diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c
index ad22162b0e..695214f192 100644
--- a/libavcodec/allcodecs.c
+++ b/libavcodec/allcodecs.c
@@ -28,11 +28,13 @@
 #include <string.h>
 
 #include "config.h"
+#include "libavutil/avassert.h"
 #include "libavutil/thread.h"
 #include "avcodec.h"
 #include "codec.h"
 #include "codec_id.h"
 #include "codec_internal.h"
+#include "codec_desc.h"
 
 extern const FFCodec ff_a64multi_encoder;
 extern const FFCodec ff_a64multi5_encoder;
@@ -1018,6 +1020,7 @@ static enum AVCodecID remap_deprecated_codec_id(enum 
AVCodecID id)
 static const AVCodec *find_codec(enum AVCodecID id, int (*x)(const AVCodec *))
 {
     const AVCodec *p, *experimental = NULL;
+    av_unused const AVCodecDescriptor *desc = avcodec_descriptor_get(id);
     void *i = 0;
 
     id = remap_deprecated_codec_id(id);
@@ -1026,6 +1029,7 @@ static const AVCodec *find_codec(enum AVCodecID id, int 
(*x)(const AVCodec *))
         if (!x(p))
             continue;
         if (p->id == id) {
+            av_assert1(!desc || !(desc->props & AV_CODEC_PROP_ENHANCEMENT));
             if (p->capabilities & AV_CODEC_CAP_EXPERIMENTAL && !experimental) {
                 experimental = p;
             } else
diff --git a/libavcodec/codec_desc.h b/libavcodec/codec_desc.h
index 96afd20208..bb74a0dc89 100644
--- a/libavcodec/codec_desc.h
+++ b/libavcodec/codec_desc.h
@@ -96,6 +96,13 @@ typedef struct AVCodecDescriptor {
  */
 #define AV_CODEC_PROP_FIELDS        (1 << 4)
 
+/**
+ * Video codec contains enhancement information meant to be applied to other
+ * existing frames, and can't output frames of its own.
+ * No standalone decoder will be available for it.
+ */
+#define AV_CODEC_PROP_ENHANCEMENT   (1 << 5)
+
 /**
  * Subtitle codec is bitmap based
  * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
-- 
2.52.0


>From 0dc065ebcc0739cde24c36bdea3345ca3d927326 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 6 Mar 2026 10:56:04 -0300
Subject: [PATCH 2/7] avcodec/parser: set avctx->pix_fmt for codecs with the
 enhancement prop set

There will be no decoder for those, so the parser's derived value may be used.

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

diff --git a/libavcodec/parser.c b/libavcodec/parser.c
index 12fdc3bb58..28f4de5826 100644
--- a/libavcodec/parser.c
+++ b/libavcodec/parser.c
@@ -28,6 +28,7 @@
 #include "libavutil/avassert.h"
 #include "libavutil/mem.h"
 
+#include "codec_desc.h"
 #include "parser.h"
 #include "parser_internal.h"
 
@@ -125,6 +126,7 @@ int av_parser_parse2(AVCodecParserContext *s, 
AVCodecContext *avctx,
                      const uint8_t *buf, int buf_size,
                      int64_t pts, int64_t dts, int64_t pos)
 {
+    const AVCodecDescriptor *desc;
     int index, i;
     uint8_t dummy_buf[AV_INPUT_BUFFER_PADDING_SIZE];
 
@@ -139,6 +141,8 @@ int av_parser_parse2(AVCodecParserContext *s, 
AVCodecContext *avctx,
                avctx->codec_id == s->parser->codec_ids[5] ||
                avctx->codec_id == s->parser->codec_ids[6]);
 
+    desc = avcodec_descriptor_get(avctx->codec_id);
+
     if (!(s->flags & PARSER_FLAG_FETCHED_OFFSET)) {
         s->next_frame_offset =
         s->cur_offset        = pos;
@@ -178,6 +182,8 @@ int av_parser_parse2(AVCodecParserContext *s, 
AVCodecContext *avctx,
         FILL(coded_height);
         FILL(width);
         FILL(height);
+        if (desc && (desc->props & AV_CODEC_PROP_ENHANCEMENT) &&
+            s->format >= 0 && avctx->pix_fmt < 0) avctx->pix_fmt = s->format;
     }
 
     /* update the file pointer */
-- 
2.52.0


>From d24d5c0a2eb937282c5ba7b1bb8690ef294ffeb7 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 6 Mar 2026 10:26:11 -0300
Subject: [PATCH 3/7] fftools/ffmpeg_mux_init: don't autoselect video codecs
 known to lack decoders

They should not be given priority even in a stream copy scenario.

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

diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index 5c0c8ca36f..7e51ce76ad 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -1652,10 +1652,12 @@ static int map_auto_video(Muxer *mux, const 
OptionsContext *o)
         }
         for (int i = 0; i < ifile->nb_streams; i++) {
             InputStream *ist = ifile->streams[i];
+            const AVCodecDescriptor *desc = 
avcodec_descriptor_get(ist->st->codecpar->codec_id);
             int64_t score;
 
             if (ist->user_set_discard == AVDISCARD_ALL ||
-                ist->st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO)
+                ist->st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO ||
+                (desc && (desc->props & AV_CODEC_PROP_ENHANCEMENT)))
                 continue;
 
             score = ist->st->codecpar->width * 
(int64_t)ist->st->codecpar->height
-- 
2.52.0


>From 6f0a26d586451f08979d1fdf982711db9d1d7afe Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 6 Mar 2026 10:28:52 -0300
Subject: [PATCH 4/7] avcodec/codec_desc: make LCEVC a video media type

Every container signals it as such, and the data media stream type is too
undefined and unsupported across the codebase that even if no standalone
decoder can be present for it, it's preferable to it.

This is technically an API break, but LCEVC support has been minimal until
now so it should be safe.

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/codec_desc.c | 3 ++-
 libavformat/avformat.h  | 5 ++---
 libavformat/mov.c       | 4 ----
 3 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/libavcodec/codec_desc.c b/libavcodec/codec_desc.c
index b3f4e73e1d..a9f21f8152 100644
--- a/libavcodec/codec_desc.c
+++ b/libavcodec/codec_desc.c
@@ -3838,9 +3838,10 @@ static const AVCodecDescriptor codec_descriptors[] = {
     },
     {
         .id        = AV_CODEC_ID_LCEVC,
-        .type      = AVMEDIA_TYPE_DATA,
+        .type      = AVMEDIA_TYPE_VIDEO,
         .name      = "lcevc",
         .long_name = NULL_IF_CONFIG_SMALL("LCEVC (Low Complexity Enhancement 
Video Coding) / MPEG-5 LCEVC / MPEG-5 part 2"),
+        .props     = AV_CODEC_PROP_ENHANCEMENT,
     },
     {
         .id        = AV_CODEC_ID_SMPTE_436M_ANC,
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index 76c251ac02..ee0ddddf8d 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -1065,9 +1065,8 @@ typedef struct AVStreamGroupTileGrid {
  * AVStreamGroupLCEVC is meant to define the relation between video streams
  * and a data stream containing LCEVC enhancement layer NALUs.
  *
- * No more than one stream of @ref AVCodecParameters.codec_type "codec_type"
- * AVMEDIA_TYPE_DATA shall be present, and it must be of
- * @ref AVCodecParameters.codec_id "codec_id" AV_CODEC_ID_LCEVC.
+ * No more than one stream of
+ * @ref AVCodecParameters.codec_id "codec_id" AV_CODEC_ID_LCEVC shall be 
present.
  */
 typedef struct AVStreamGroupLCEVC {
     const AVClass *av_class;
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 1ae281440e..9b7df252b2 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -10802,8 +10802,6 @@ static int mov_parse_lcevc_streams(AVFormatContext *s)
             !(sc->tref_flags & MOV_TREF_FLAG_ENHANCEMENT))
             continue;
 
-        st->codecpar->codec_type = AVMEDIA_TYPE_DATA;
-
         stg = avformat_stream_group_create(s, AV_STREAM_GROUP_PARAMS_LCEVC, 
NULL);
         if (!stg)
             return AVERROR(ENOMEM);
@@ -10811,8 +10809,6 @@ static int mov_parse_lcevc_streams(AVFormatContext *s)
         stg->id = st->id;
         stg->params.lcevc->width  = st->codecpar->width;
         stg->params.lcevc->height = st->codecpar->height;
-        st->codecpar->width = 0;
-        st->codecpar->height = 0;
 
         while (st_base = mov_find_reference_track(s, st, j)) {
             err = avformat_stream_group_add_stream(stg, st_base);
-- 
2.52.0


>From 6a260553ac5a35777dec76b5d762329d157c7708 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Wed, 4 Feb 2026 10:26:52 -0300
Subject: [PATCH 5/7] avcodec/bsf: add a LCEVC merging bsf

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/bitstream_filters.c |   1 +
 libavcodec/bsf/Makefile        |   1 +
 libavcodec/bsf/lcevc_merge.c   | 241 +++++++++++++++++++++++++++++++++
 3 files changed, 243 insertions(+)
 create mode 100644 libavcodec/bsf/lcevc_merge.c

diff --git a/libavcodec/bitstream_filters.c b/libavcodec/bitstream_filters.c
index 150cca8939..bcb7a9f897 100644
--- a/libavcodec/bitstream_filters.c
+++ b/libavcodec/bitstream_filters.c
@@ -48,6 +48,7 @@ extern const FFBitStreamFilter ff_hapqa_extract_bsf;
 extern const FFBitStreamFilter ff_hevc_metadata_bsf;
 extern const FFBitStreamFilter ff_hevc_mp4toannexb_bsf;
 extern const FFBitStreamFilter ff_imx_dump_header_bsf;
+extern const FFBitStreamFilter ff_lcevc_merge_bsf;
 extern const FFBitStreamFilter ff_lcevc_metadata_bsf;
 extern const FFBitStreamFilter ff_media100_to_mjpegb_bsf;
 extern const FFBitStreamFilter ff_mjpeg2jpeg_bsf;
diff --git a/libavcodec/bsf/Makefile b/libavcodec/bsf/Makefile
index eb090090ef..ba01b2a944 100644
--- a/libavcodec/bsf/Makefile
+++ b/libavcodec/bsf/Makefile
@@ -25,6 +25,7 @@ OBJS-$(CONFIG_HEVC_METADATA_BSF)          += 
bsf/h265_metadata.o
 OBJS-$(CONFIG_DOVI_RPU_BSF)               += bsf/dovi_rpu.o
 OBJS-$(CONFIG_HEVC_MP4TOANNEXB_BSF)       += bsf/hevc_mp4toannexb.o
 OBJS-$(CONFIG_IMX_DUMP_HEADER_BSF)        += bsf/imx_dump_header.o
+OBJS-$(CONFIG_LCEVC_MERGE_BSF)            += bsf/lcevc_merge.o
 OBJS-$(CONFIG_LCEVC_METADATA_BSF)         += bsf/lcevc_metadata.o
 OBJS-$(CONFIG_MEDIA100_TO_MJPEGB_BSF)     += bsf/media100_to_mjpegb.o
 OBJS-$(CONFIG_MJPEG2JPEG_BSF)             += bsf/mjpeg2jpeg.o
diff --git a/libavcodec/bsf/lcevc_merge.c b/libavcodec/bsf/lcevc_merge.c
new file mode 100644
index 0000000000..161dfe7956
--- /dev/null
+++ b/libavcodec/bsf/lcevc_merge.c
@@ -0,0 +1,241 @@
+/*
+ * 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/avassert.h"
+#include "libavutil/container_fifo.h"
+#include "libavutil/opt.h"
+
+#include "bsf.h"
+#include "bsf_internal.h"
+#include "packet.h"
+#include "packet_internal.h"
+
+typedef struct LCEVCMergeContext {
+    AVClass *class;
+    AVContainerFifo *base_fifo, *lcevc_fifo;
+    AVPacket *buffer_pkt, *base_pkt, *lcevc_pkt;
+    int base_idx, lcevc_idx, buffer_max;
+} LCEVCMergeContext;
+
+static void *packet_alloc(void *opaque)
+{
+    return av_packet_alloc();
+}
+
+static void packet_reset(void *opaque, void *obj)
+{
+    av_packet_unref(obj);
+}
+
+static void packet_free(void *opaque, void *obj)
+{
+    AVPacket *pkt = obj;
+    av_packet_free(&pkt);
+}
+
+static int packet_transfer(void *opaque, void *dst, void *src, unsigned flags)
+{
+    if (flags & AV_CONTAINER_FIFO_FLAG_REF)
+        return av_packet_ref(dst, src);
+
+    av_packet_move_ref(dst, src);
+    return 0;
+}
+
+static void lcevc_merge_flush(AVBSFContext *bsf)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+
+    av_container_fifo_drain(ctx->base_fifo,  
av_container_fifo_can_read(ctx->base_fifo));
+    av_container_fifo_drain(ctx->lcevc_fifo, 
av_container_fifo_can_read(ctx->lcevc_fifo));
+    av_packet_unref(ctx->buffer_pkt);
+    av_packet_unref(ctx->base_pkt);
+    av_packet_unref(ctx->lcevc_pkt);
+}
+
+static int lcevc_buffer_packet(AVBSFContext *bsf, AVContainerFifo *fifo, 
AVPacket *pkt)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+    size_t cnt = av_container_fifo_can_read(fifo);
+    int ret;
+
+    if (cnt == ctx->buffer_max)
+        return 0;
+
+    if (cnt) {
+        AVPacket *peek;
+        av_container_fifo_peek(fifo, (void **)&peek, cnt - 1);
+        if (pkt->dts <= peek->dts) {
+            av_packet_unref(pkt);
+            return AVERROR(EINVAL);
+        }
+    }
+
+    ret = av_container_fifo_write(fifo, pkt, 0);
+    if (ret < 0) {
+        av_packet_unref(pkt);
+        return ret;
+    }
+
+    return 0;
+}
+
+static int lcevc_merge_packets(AVBSFContext *bsf, AVPacket *out)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+    uint8_t *buf;
+    int ret;
+
+    ret = av_container_fifo_read(ctx->base_fifo, ctx->base_pkt, 0);
+    if (ret < 0)
+        goto fail;
+    ret = av_container_fifo_read(ctx->lcevc_fifo, ctx->lcevc_pkt, 0);
+    if (ret < 0)
+        goto fail;
+
+    if (ctx->base_pkt->dts != ctx->lcevc_pkt->dts) {
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+
+    buf = av_packet_new_side_data(ctx->base_pkt, AV_PKT_DATA_LCEVC, 
ctx->lcevc_pkt->size);
+    if (!buf) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+    memcpy(buf, ctx->lcevc_pkt->data, ctx->lcevc_pkt->size);
+    av_packet_move_ref(out, ctx->base_pkt);
+
+    ret = 0;
+fail:
+    av_packet_unref(ctx->base_pkt);
+    av_packet_unref(ctx->lcevc_pkt);
+
+    return ret;
+}
+
+static int lcevc_merge_filter(AVBSFContext *bsf, AVPacket *out)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+    AVPacket *pkt = ctx->buffer_pkt;
+    int ret, stream_index = -1;
+
+    if (AVPACKET_IS_EMPTY(pkt)) {
+        ret = ff_bsf_get_packet_ref(bsf, pkt);
+        if (ret < 0 && ret != AVERROR_EOF && ret != AVERROR(EAGAIN))
+            return ret;
+        if (ret)
+            goto out;
+    }
+
+    stream_index = pkt->stream_index;
+    if (stream_index == ctx->base_idx) {
+        ret = lcevc_buffer_packet(bsf, ctx->base_fifo, pkt);
+        if (ret < 0)
+            return ret;
+    } else if (stream_index == ctx->lcevc_idx) {
+        ret = lcevc_buffer_packet(bsf, ctx->lcevc_fifo, pkt);
+        if (ret < 0)
+            return ret;
+    } else {
+        av_packet_unref(pkt);
+        return AVERROR(EINVAL);
+    }
+
+out:
+    if ((stream_index == ctx->base_idx || ret == AVERROR_EOF) &&
+        av_container_fifo_can_read(ctx->base_fifo) &&
+        av_container_fifo_can_read(ctx->lcevc_fifo))
+        ret = lcevc_merge_packets(bsf, out);
+    else if (stream_index == ctx->lcevc_idx) {
+        AVPacket *peek;
+        ret = av_container_fifo_peek(ctx->lcevc_fifo, (void **)&peek,
+                                     
av_container_fifo_can_read(ctx->lcevc_fifo) - 1);
+        av_assert0(!ret);
+        ret = av_packet_ref(out, peek);
+    } else if (ret != AVERROR_EOF)
+        ret = AVERROR(EAGAIN);
+
+    return ret;
+}
+
+static int lcevc_merge_init(AVBSFContext *bsf)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+
+    if (ctx->base_idx < 0)
+        return AVERROR(EINVAL);
+    if (ctx->lcevc_idx < 0)
+        return AVERROR(EINVAL);
+    if (ctx->base_idx == ctx->lcevc_idx)
+        return AVERROR(EINVAL);
+
+    ctx->buffer_pkt = av_packet_alloc();
+    ctx->base_pkt   = av_packet_alloc();
+    ctx->lcevc_pkt  = av_packet_alloc();
+    ctx->base_fifo  = av_container_fifo_alloc(NULL, packet_alloc, packet_reset,
+                                              packet_free, packet_transfer, 0);
+    ctx->lcevc_fifo = av_container_fifo_alloc(NULL, packet_alloc, packet_reset,
+                                              packet_free, packet_transfer, 0);
+
+    if (!ctx->buffer_pkt || !ctx->base_pkt || !ctx->lcevc_pkt ||
+        !ctx->base_fifo || !ctx->lcevc_fifo)
+        return AVERROR(ENOMEM);
+
+    return 0;
+}
+
+static void lcevc_merge_close(AVBSFContext *bsf)
+{
+    LCEVCMergeContext *ctx = bsf->priv_data;
+
+    av_container_fifo_free(&ctx->base_fifo);
+    av_container_fifo_free(&ctx->lcevc_fifo);
+    av_packet_free(&ctx->buffer_pkt);
+    av_packet_free(&ctx->base_pkt);
+    av_packet_free(&ctx->lcevc_pkt);
+}
+
+#define OFFSET(x) offsetof(LCEVCMergeContext, x)
+#define FLAGS (AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_BSF_PARAM)
+static const AVOption lcevc_merge_options[] = {
+    { "base_idx",  "Base stream index",
+        OFFSET(base_idx),  AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, FLAGS 
},
+    { "lcevc_idx", "LCEVC stream index",
+        OFFSET(lcevc_idx), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, FLAGS 
},
+    { "max_packet_buffer", "Maximum amount of packets to buffer",
+        OFFSET(buffer_max), AV_OPT_TYPE_INT, { .i64 = 16 }, 1, INT_MAX, FLAGS 
},
+    { NULL }
+};
+
+static const AVClass lcevc_merge_class = {
+    .class_name = "lcevc_merge_bsf",
+    .item_name  = av_default_item_name,
+    .option     = lcevc_merge_options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+const FFBitStreamFilter ff_lcevc_merge_bsf = {
+    .p.name         = "lcevc_merge",
+    .p.priv_class   = &lcevc_merge_class,
+    .priv_data_size = sizeof(LCEVCMergeContext),
+    .init           = lcevc_merge_init,
+    .flush          = lcevc_merge_flush,
+    .close          = lcevc_merge_close,
+    .filter         = lcevc_merge_filter,
+};
-- 
2.52.0


>From fc203246f82d91c9aece7e27178a89be437ee09d Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Wed, 4 Feb 2026 10:26:58 -0300
Subject: [PATCH 6/7] fftools/ffmpeg_demux: allow Stream Groups to insert
 bitstream filters

Signed-off-by: James Almer <[email protected]>
---
 fftools/ffmpeg_demux.c | 131 ++++++++++++++++++++++++++++-------------
 1 file changed, 90 insertions(+), 41 deletions(-)

diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
index 7c708ff0f3..794e3c3bd6 100644
--- a/fftools/ffmpeg_demux.c
+++ b/fftools/ffmpeg_demux.c
@@ -40,6 +40,16 @@
 
 #include "libavformat/avformat.h"
 
+typedef struct DemuxStreamGroup {
+    InputStreamGroup         istg;
+
+    // name used for logging
+    char                     log_name[32];
+
+    AVBSFList               *lst;
+    AVBSFContext            *bsf;
+} DemuxStreamGroup;
+
 typedef struct DemuxStream {
     InputStream              ist;
 
@@ -92,6 +102,9 @@ typedef struct DemuxStream {
 
     AVBSFContext            *bsf;
 
+    DemuxStreamGroup       **dsg;
+    int                      nb_dsg;
+
     /* number of packets successfully read for this stream */
     uint64_t                 nb_packets;
     // combined size of all the packets read
@@ -104,13 +117,6 @@ typedef struct DemuxStream {
     int64_t                  lag;
 } DemuxStream;
 
-typedef struct DemuxStreamGroup {
-    InputStreamGroup         istg;
-
-    // name used for logging
-    char                     log_name[32];
-} DemuxStreamGroup;
-
 typedef struct Demuxer {
     InputFile             f;
 
@@ -160,6 +166,11 @@ typedef struct DemuxThreadContext {
     AVPacket *pkt_bsf;
 } DemuxThreadContext;
 
+static DemuxStreamGroup *dsg_from_istg(InputStreamGroup *istg)
+{
+    return (DemuxStreamGroup*)istg;
+}
+
 static DemuxStream *ds_from_ist(InputStream *ist)
 {
     return (DemuxStream*)ist;
@@ -582,6 +593,55 @@ static int do_send(Demuxer *d, DemuxStream *ds, AVPacket 
*pkt, unsigned flags,
     return 0;
 }
 
+static int do_bsf(Demuxer *d, DemuxThreadContext *dt, DemuxStreamGroup *dsg,
+                  DemuxStream *ds, AVBSFContext *bsf, AVPacket *pkt)
+{
+    void *logctx = dsg ? (void *)dsg : (void *)ds;
+    int ret;
+
+        if (pkt)
+            av_packet_rescale_ts(pkt, pkt->time_base, bsf->time_base_in);
+
+        ret = av_bsf_send_packet(bsf, pkt);
+        if (ret < 0) {
+            if (pkt)
+                av_packet_unref(pkt);
+            av_log(logctx, AV_LOG_ERROR, "Error submitting a packet for 
filtering: %s\n",
+                   av_err2str(ret));
+            return ret;
+        }
+
+        while (1) {
+            ret = av_bsf_receive_packet(bsf, dt->pkt_bsf);
+            if (ret == AVERROR(EAGAIN))
+                return 0;
+            else if (ret < 0) {
+                if (ret != AVERROR_EOF)
+                    av_log(logctx, AV_LOG_ERROR,
+                           "Error applying bitstream filters to a packet: 
%s\n",
+                           av_err2str(ret));
+                return ret;
+            }
+            if (ds->discard) {
+                av_packet_unref(dt->pkt_bsf);
+                continue;
+            }
+
+            dt->pkt_bsf->time_base = bsf->time_base_out;
+
+            if (dsg && ds->bsf)
+                ret = do_bsf(d, dt, NULL, ds, ds->bsf, pkt ? dt->pkt_bsf : 
NULL);
+            else
+                ret = do_send(d, ds, dt->pkt_bsf, 0, "filtered");
+            if (ret < 0) {
+                av_packet_unref(dt->pkt_bsf);
+                return ret;
+            }
+        }
+
+    return 0;
+}
+
 static int demux_send(Demuxer *d, DemuxThreadContext *dt, DemuxStream *ds,
                       AVPacket *pkt, unsigned flags)
 {
@@ -610,39 +670,13 @@ static int demux_send(Demuxer *d, DemuxThreadContext *dt, 
DemuxStream *ds,
     }
 
     if (ds->bsf) {
-        if (pkt)
-            av_packet_rescale_ts(pkt, pkt->time_base, ds->bsf->time_base_in);
-
-        ret = av_bsf_send_packet(ds->bsf, pkt);
-        if (ret < 0) {
-            if (pkt)
-                av_packet_unref(pkt);
-            av_log(ds, AV_LOG_ERROR, "Error submitting a packet for filtering: 
%s\n",
-                   av_err2str(ret));
+        AVBSFContext *bsf = ds->bsf;
+        ret = do_bsf(d, dt, NULL, ds, bsf, pkt);
+        if (ret < 0)
             return ret;
-        }
-
-        while (1) {
-            ret = av_bsf_receive_packet(ds->bsf, dt->pkt_bsf);
-            if (ret == AVERROR(EAGAIN))
-                return 0;
-            else if (ret < 0) {
-                if (ret != AVERROR_EOF)
-                    av_log(ds, AV_LOG_ERROR,
-                           "Error applying bitstream filters to a packet: 
%s\n",
-                           av_err2str(ret));
-                return ret;
-            }
-
-            dt->pkt_bsf->time_base = ds->bsf->time_base_out;
-
-            ret = do_send(d, ds, dt->pkt_bsf, 0, "filtered");
-            if (ret < 0) {
-                av_packet_unref(dt->pkt_bsf);
-                return ret;
-            }
-        }
-    } else {
+    } else if (ds->discard && pkt) {
+        av_packet_unref(pkt);
+    } else{
         ret = do_send(d, ds, pkt, flags, "demuxed");
         if (ret < 0)
             return ret;
@@ -670,7 +704,17 @@ static int demux_bsf_flush(Demuxer *d, DemuxThreadContext 
*dt)
             return ret;
         }
 
-        av_bsf_flush(ds->bsf);
+        if (ds->bsf)
+            av_bsf_flush(ds->bsf);
+    }
+
+    for (unsigned i = 0; i < f->nb_stream_groups; i++) {
+        DemuxStreamGroup *dsg = dsg_from_istg(f->stream_groups[i]);
+
+        if (!dsg->bsf)
+            continue;
+
+        av_bsf_flush(dsg->bsf);
     }
 
     return 0;
@@ -793,7 +837,7 @@ static int input_thread(void *arg)
            dynamically in stream : we ignore them */
         ds = dt.pkt_demux->stream_index < f->nb_streams ?
              ds_from_ist(f->streams[dt.pkt_demux->stream_index]) : NULL;
-        if (!ds || ds->discard || ds->finished) {
+        if (!ds || ds->finished) {
             report_new_stream(d, dt.pkt_demux);
             av_packet_unref(dt.pkt_demux);
             continue;
@@ -899,9 +943,14 @@ static void ist_free(InputStream **pist)
 static void istg_free(InputStreamGroup **pistg)
 {
     InputStreamGroup *istg = *pistg;
+    DemuxStreamGroup *dsg;
 
     if (!istg)
         return;
+    dsg = dsg_from_istg(istg);
+
+    av_bsf_list_free(&dsg->lst);
+    av_bsf_free(&dsg->bsf);
 
     av_freep(pistg);
 }
-- 
2.52.0


>From 355301846fab72e6d53fc6b2b05fd3714b6af59b Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Thu, 5 Feb 2026 20:53:39 -0300
Subject: [PATCH 7/7] fftools/ffmpeg_demux: merge LCEVC streams with the base
 stream

Signed-off-by: James Almer <[email protected]>
---
 fftools/ffmpeg_demux.c | 121 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 116 insertions(+), 5 deletions(-)

diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c
index 794e3c3bd6..ded83d80ce 100644
--- a/fftools/ffmpeg_demux.c
+++ b/fftools/ffmpeg_demux.c
@@ -646,10 +646,20 @@ static int demux_send(Demuxer *d, DemuxThreadContext *dt, 
DemuxStream *ds,
                       AVPacket *pkt, unsigned flags)
 {
     InputFile  *f = &d->f;
+    DemuxStreamGroup *dsg = NULL;
     int ret;
 
+    for (int i = 0; i < ds->nb_dsg; i++) {
+        const InputStreamGroup *istg = &ds->dsg[i]->istg;
+
+        if (istg->stg->type != AV_STREAM_GROUP_PARAMS_LCEVC)
+            continue;
+        dsg = ds->dsg[i];
+        break;
+    }
+
     // pkt can be NULL only when flushing BSFs
-    av_assert0(ds->bsf || pkt);
+    av_assert0(ds->bsf || (dsg && dsg->bsf) || pkt);
 
     // send heartbeat for sub2video streams
     if (d->pkt_heartbeat && pkt && pkt->pts != AV_NOPTS_VALUE) {
@@ -669,9 +679,9 @@ static int demux_send(Demuxer *d, DemuxThreadContext *dt, 
DemuxStream *ds,
         }
     }
 
-    if (ds->bsf) {
-        AVBSFContext *bsf = ds->bsf;
-        ret = do_bsf(d, dt, NULL, ds, bsf, pkt);
+    if (ds->bsf || (dsg && dsg->bsf)) {
+        AVBSFContext *bsf = dsg ? dsg->bsf : ds->bsf;
+        ret = do_bsf(d, dt, dsg, ds, bsf, pkt);
         if (ret < 0)
             return ret;
     } else if (ds->discard && pkt) {
@@ -692,8 +702,18 @@ static int demux_bsf_flush(Demuxer *d, DemuxThreadContext 
*dt)
 
     for (unsigned i = 0; i < f->nb_streams; i++) {
         DemuxStream *ds = ds_from_ist(f->streams[i]);
+        DemuxStreamGroup *dsg = NULL;
 
-        if (!ds->bsf)
+        for (int j = 0; j < ds->nb_dsg; j++) {
+           const InputStreamGroup *istg = &ds->dsg[j]->istg;
+
+            if (istg->stg->type != AV_STREAM_GROUP_PARAMS_LCEVC)
+                continue;
+            dsg = ds->dsg[j];
+            break;
+        }
+
+        if (!ds->bsf && (!dsg || !dsg->bsf))
             continue;
 
         ret = demux_send(d, dt, ds, NULL, 0);
@@ -1017,6 +1037,38 @@ int ist_use(InputStream *ist, int decoding_needed,
     ds->decoding_needed   |= decoding_needed;
     ds->streamcopy_needed |= !decoding_needed;
 
+    for (int i = 0; i < ds->nb_dsg; i++) {
+        DemuxStreamGroup *dsg = ds->dsg[i];
+        const InputStreamGroup *istg = &dsg->istg;
+
+        if (istg->stg->type != AV_STREAM_GROUP_PARAMS_LCEVC)
+            continue;
+        const AVStreamGroupLCEVC *lcevc = istg->stg->params.lcevc;
+        if (ist->st->index == istg->stg->streams[lcevc->lcevc_index]->index)
+            break;
+
+        av_assert0(dsg->lst);
+        ret = av_bsf_list_finalize(&dsg->lst, &dsg->bsf);
+        if (ret < 0) {
+            av_bsf_list_free(&dsg->lst);
+            av_log(ist, AV_LOG_ERROR,
+                   "Error finalizing bitstream filter list: %s\n", 
av_err2str(ret));
+            return ret;
+        }
+
+        ret = av_bsf_init(dsg->bsf);
+        if (ret < 0) {
+            av_log(dsg, AV_LOG_ERROR, "Error initializing bitstream filters: 
%s\n",
+                   av_err2str(ret));
+            return ret;
+        }
+
+        InputStream *lcevc_ist = 
d->f.streams[istg->stg->streams[lcevc->lcevc_index]->index];
+        lcevc_ist->st->discard = 0;
+
+        break;
+    }
+
     if (decoding_needed && ds->sch_idx_dec < 0) {
         int is_audio = ist->st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
         int is_unreliable = !!(d->f.ctx->iformat->flags & AVFMT_NOTIMESTAMPS);
@@ -1772,8 +1824,49 @@ fail:
     return ret;
 }
 
+static int istg_parse_lcevc(const OptionsContext *o, Demuxer *d, 
DemuxStreamGroup *dsg)
+{
+    InputFile *f = &d->f;
+    const AVStreamGroup *stg = dsg->istg.stg;
+    const AVStreamGroupLCEVC *lcevc = stg->params.lcevc;
+    const AVBitStreamFilter *filter = av_bsf_get_by_name("lcevc_merge");
+    const InputStream *ist = 
f->streams[stg->streams[!lcevc->lcevc_index]->index];
+    AVBSFContext *bsf;
+    int ret;
+
+    if (!filter || stg->nb_streams != 2)
+        return 0;
+
+    dsg->lst = av_bsf_list_alloc();
+    if (!dsg->lst)
+        return AVERROR(ENOMEM);
+
+    ret = av_bsf_alloc(filter, &bsf);
+    if (ret < 0)
+        return ret;
+
+    av_opt_set_int(bsf->priv_data, "base_idx", !lcevc->lcevc_index, 0);
+    av_opt_set_int(bsf->priv_data, "lcevc_idx", lcevc->lcevc_index, 0);
+
+    ret = avcodec_parameters_copy(bsf->par_in, ist->par);
+    if (ret < 0)
+        return ret;
+    bsf->time_base_in = ist->st->time_base;
+
+    ret = av_bsf_list_append(dsg->lst, bsf);
+    if (ret < 0) {
+        av_bsf_free(&bsf);
+        av_log(dsg, AV_LOG_ERROR,
+               "Error adding bitstream filter '%s': %s\n",
+               filter->name, av_err2str(ret));
+    }
+
+    return 0;
+}
+
 static int istg_add(const OptionsContext *o, Demuxer *d, AVStreamGroup *stg)
 {
+    InputFile *f = &d->f;
     DemuxStreamGroup *dsg;
     InputStreamGroup *istg;
     int ret;
@@ -1784,12 +1877,30 @@ static int istg_add(const OptionsContext *o, Demuxer 
*d, AVStreamGroup *stg)
 
     istg = &dsg->istg;
 
+    switch (stg->type) {
+    case AV_STREAM_GROUP_PARAMS_LCEVC:
+        for (int i = 0; i < istg->stg->nb_streams; i++) {
+            DemuxStream *ds = 
ds_from_ist(f->streams[istg->stg->streams[i]->index]);
+            ret = av_dynarray_add_nofree(&ds->dsg, &ds->nb_dsg, dsg);
+            if (ret < 0)
+                return ret;
+        }
+        break;
+    default:
+        break;
+    }
+
     switch (stg->type) {
     case AV_STREAM_GROUP_PARAMS_TILE_GRID:
         ret = istg_parse_tile_grid(o, d, istg);
         if (ret < 0)
             return ret;
         break;
+    case AV_STREAM_GROUP_PARAMS_LCEVC:
+        ret = istg_parse_lcevc(o, d, dsg);
+        if (ret < 0)
+            return ret;
+        break;
     default:
         break;
     }
-- 
2.52.0

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

Reply via email to