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

When trying to prevent the muxer from writing a sbas type tref entry if there 
was no stream to reference at all, i stumbled upon the fact we're blindly 
writing tref entries of type tmcd referencing the very first track (regardless 
of what kind) if they are set by the user beforehand, as is the case of a 
stream copy scenario.
More than that, both the demuxer and muxer don't support more than one entry 
per tref type, resulting in bogus stream copies like is the case of 
fate-suite/mov/fcp_export8-236.mov, where a single video track references three 
timecode tracks, or fate-suite/mov/mp4-with-mov-in24-ver.mp4, where a video and 
audio tracks both reference the same timecode track.

This PR tries to implement this by means of adding a new type of Stream Group, 
which will contain a single timecode stream and all the streams that reference 
it, while not breaking the existing behavior for users unaware of said group 
type.


>From f96fd8175f6774943b4043851c8e201ac88943ac Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Tue, 17 Mar 2026 12:01:46 -0300
Subject: [PATCH 1/4] avformat/mov: support storing more than one tref_id entry

And rename it to sbas_id, given it's only used for that tref type.
No more than one entry will be stored even after this change, which is done
to simplify a following commit.

Signed-off-by: James Almer <[email protected]>
---
 libavformat/isom.h |  6 ++----
 libavformat/mov.c  | 25 ++++++++++++++++---------
 2 files changed, 18 insertions(+), 13 deletions(-)

diff --git a/libavformat/isom.h b/libavformat/isom.h
index d7e138585a..070389c665 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -222,8 +222,8 @@ typedef struct MOVStreamContext {
     unsigned drefs_count;
     MOVDref *drefs;
     int dref_id;
-    unsigned tref_flags;
-    int tref_id;
+    int nb_sbas_id;
+    uint32_t *sbas_id;
     int timecode_track;
     int width;            ///< tkhd width
     int height;           ///< tkhd height
@@ -434,8 +434,6 @@ void ff_mp4_parse_es_descr(AVIOContext *pb, int *es_id);
 #define MOV_SAMPLE_DEPENDENCY_YES     0x1
 #define MOV_SAMPLE_DEPENDENCY_NO      0x2
 
-#define MOV_TREF_FLAG_ENHANCEMENT     0x1
-
 #define TAG_IS_AVCI(tag)                    \
     ((tag) == MKTAG('a', 'i', '5', 'p') ||  \
      (tag) == MKTAG('a', 'i', '5', 'q') ||  \
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 9e1aca1b78..75b23481cd 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -2561,11 +2561,19 @@ static int mov_read_sbas(MOVContext* c, AVIOContext* 
pb, MOVAtom atom)
         av_log(c->fc, AV_LOG_ERROR, "Only a single tref of type sbas is 
supported\n");
         return AVERROR_PATCHWELCOME;
     }
+    if (atom.size < 4)
+        return AVERROR_INVALIDDATA;
 
     st = c->fc->streams[c->fc->nb_streams - 1];
     sc = st->priv_data;
-    sc->tref_id = avio_rb32(pb);
-    sc->tref_flags |= MOV_TREF_FLAG_ENHANCEMENT;
+
+    int err = av_reallocp(&sc->sbas_id, atom.size);
+    if (err < 0)
+        return err;
+
+    sc->nb_sbas_id = 1;
+    for (int i = 0; i < sc->nb_sbas_id; i++)
+        sc->sbas_id[i] = avio_rb32(pb);
 
     return 0;
 }
@@ -5202,8 +5210,6 @@ static int mov_read_trak(MOVContext *c, AVIOContext *pb, 
MOVAtom atom)
     st->codecpar->codec_type = AVMEDIA_TYPE_DATA;
     sc->ffindex = st->index;
     c->trak_index = st->index;
-    sc->tref_flags = 0;
-    sc->tref_id = -1;
     sc->refcount = 1;
 
     if ((ret = mov_read_default(c, pb, atom)) < 0)
@@ -10083,6 +10089,7 @@ static void mov_free_stream_context(AVFormatContext *s, 
AVStream *st)
     av_freep(&sc->open_key_samples);
     av_freep(&sc->display_matrix);
     av_freep(&sc->index_ranges);
+    av_freep(&sc->sbas_id);
 
     if (sc->extradata)
         for (int i = 0; i < sc->stsd_count; i++)
@@ -10782,12 +10789,13 @@ static AVStream 
*mov_find_reference_track(AVFormatContext *s, AVStream *st,
 {
     MOVStreamContext *sc = st->priv_data;
 
-    if (sc->tref_id < 0)
+    if (!sc->nb_sbas_id)
         return NULL;
 
     for (int i = first_index; i < s->nb_streams; i++)
-        if (s->streams[i]->id == sc->tref_id)
-            return s->streams[i];
+        for (int j = 0; j < sc->nb_sbas_id; j++)
+            if (s->streams[i]->id == sc->sbas_id[j])
+                return s->streams[i];
 
     return NULL;
 }
@@ -10804,8 +10812,7 @@ static int mov_parse_lcevc_streams(AVFormatContext *s)
         int j = 0;
 
         /* Find an enhancement stream. */
-        if (st->codecpar->codec_id != AV_CODEC_ID_LCEVC ||
-            !(sc->tref_flags & MOV_TREF_FLAG_ENHANCEMENT))
+        if (st->codecpar->codec_id != AV_CODEC_ID_LCEVC || !sc->nb_sbas_id)
             continue;
 
         stg = avformat_stream_group_create(s, AV_STREAM_GROUP_PARAMS_LCEVC, 
NULL);
-- 
2.52.0


>From 932632e216de109a9188b7b2b40883093fc99de1 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Tue, 17 Mar 2026 10:57:32 -0300
Subject: [PATCH 2/4] avformat/avformat: add a Timecode metadata Stream Group

Signed-off-by: James Almer <[email protected]>
---
 fftools/ffmpeg_mux_init.c |  3 +++
 libavformat/avformat.c    |  5 +++++
 libavformat/avformat.h    | 19 +++++++++++++++++++
 libavformat/dump.c        | 10 ++++++++++
 libavformat/options.c     | 26 ++++++++++++++++++++++++++
 5 files changed, 63 insertions(+)

diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index e0eecf78f2..b3405d90b6 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -2553,6 +2553,7 @@ static int of_map_group(Muxer *mux, AVDictionary **dict, 
AVBPrint *bp, const cha
         break;
     }
     case AV_STREAM_GROUP_PARAMS_LCEVC:
+    case AV_STREAM_GROUP_PARAMS_TMCD:
         break;
     default:
         av_log(mux, AV_LOG_ERROR, "Unsupported mapped group type %d.\n", 
stg->type);
@@ -2578,6 +2579,8 @@ static int of_parse_group_token(Muxer *mux, const char 
*token, char *ptr)
                 { .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" },
+            { "tmcd", NULL, 0, AV_OPT_TYPE_CONST,
+                { .i64 = AV_STREAM_GROUP_PARAMS_TMCD }, .unit = "type" },
         { NULL },
     };
     const AVClass class = {
diff --git a/libavformat/avformat.c b/libavformat/avformat.c
index 8a98f2e53e..431aae8240 100644
--- a/libavformat/avformat.c
+++ b/libavformat/avformat.c
@@ -106,6 +106,10 @@ void ff_free_stream_group(AVStreamGroup **pstg)
         av_opt_free(stg->params.lcevc);
         av_freep(&stg->params.lcevc);
         break;
+    case AV_STREAM_GROUP_PARAMS_TMCD:
+        av_opt_free(stg->params.tmcd);
+        av_freep(&stg->params.tmcd);
+        break;
     default:
         break;
     }
@@ -264,6 +268,7 @@ const char *avformat_stream_group_name(enum 
AVStreamGroupParamsType type)
     case AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION:     return "IAMF Mix 
Presentation";
     case AV_STREAM_GROUP_PARAMS_TILE_GRID:                 return "Tile Grid";
     case AV_STREAM_GROUP_PARAMS_LCEVC:                     return "LCEVC 
(Split video and enhancement)";
+    case AV_STREAM_GROUP_PARAMS_TMCD:                      return "Timecode 
Metadata";
     }
     return NULL;
 }
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index ee0ddddf8d..4c6e1103b3 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -1085,12 +1085,30 @@ typedef struct AVStreamGroupLCEVC {
     int height;
 } AVStreamGroupLCEVC;
 
+/**
+ * AVStreamGroupTMCD is meant to define the relation between a video, audio,
+ * or subtitle stream, and a data stream containing timecode metadata.
+ *
+ * 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_NONE.
+ */
+typedef struct AVStreamGroupTMCD {
+    const AVClass *av_class;
+
+    /**
+     * Index of the timecode metadata stream in the AVStreamGroup.
+     */
+    unsigned int tmcd_index;
+} AVStreamGroupTMCD;
+
 enum AVStreamGroupParamsType {
     AV_STREAM_GROUP_PARAMS_NONE,
     AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT,
     AV_STREAM_GROUP_PARAMS_IAMF_MIX_PRESENTATION,
     AV_STREAM_GROUP_PARAMS_TILE_GRID,
     AV_STREAM_GROUP_PARAMS_LCEVC,
+    AV_STREAM_GROUP_PARAMS_TMCD,
 };
 
 struct AVIAMFAudioElement;
@@ -1133,6 +1151,7 @@ typedef struct AVStreamGroup {
         struct AVIAMFMixPresentation *iamf_mix_presentation;
         struct AVStreamGroupTileGrid *tile_grid;
         struct AVStreamGroupLCEVC *lcevc;
+        struct AVStreamGroupTMCD *tmcd;
     } params;
 
     /**
diff --git a/libavformat/dump.c b/libavformat/dump.c
index 66abc2af7e..21393a3fa3 100644
--- a/libavformat/dump.c
+++ b/libavformat/dump.c
@@ -837,6 +837,16 @@ static void dump_stream_group(const AVFormatContext *ic, 
uint8_t *printed,
         }
         break;
     }
+    case AV_STREAM_GROUP_PARAMS_TMCD: {
+        const AVStreamGroupTMCD *tmcd = stg->params.tmcd;
+        av_log(NULL, AV_LOG_INFO, " Timecode Metadata:\n");
+        for (int i = 0; i < stg->nb_streams; i++) {
+            const AVStream *st = stg->streams[i];
+            dump_stream_format(ic, st->index, i, index, is_output, 
AV_LOG_INFO);
+            printed[st->index] = 1;
+        }
+        break;
+    }
     default:
         break;
     }
diff --git a/libavformat/options.c b/libavformat/options.c
index 72a6a5a43e..0198e472d6 100644
--- a/libavformat/options.c
+++ b/libavformat/options.c
@@ -365,6 +365,20 @@ static const AVClass lcevc_class = {
     .option     = lcevc_options,
 };
 
+#define OFFSET(x) offsetof(AVStreamGroupTMCD, x)
+static const AVOption tmcd_options[] = {
+    { "tmcd_index", "Index of the timecode stream within the group", 
OFFSET(tmcd_index),
+        AV_OPT_TYPE_INT, { .i64 = 0 }, 0, INT_MAX, FLAGS },
+    { NULL },
+};
+#undef OFFSET
+
+static const AVClass tmcd_class = {
+    .class_name = "AVStreamGroupTMCD",
+    .version    = LIBAVUTIL_VERSION_INT,
+    .option     = tmcd_options,
+};
+
 static void *stream_group_child_next(void *obj, void *prev)
 {
     AVStreamGroup *stg = obj;
@@ -378,6 +392,8 @@ static void *stream_group_child_next(void *obj, void *prev)
             return stg->params.tile_grid;
         case AV_STREAM_GROUP_PARAMS_LCEVC:
             return stg->params.lcevc;
+        case AV_STREAM_GROUP_PARAMS_TMCD:
+            return stg->params.tmcd;
         default:
             break;
         }
@@ -408,6 +424,9 @@ static const AVClass *stream_group_child_iterate(void 
**opaque)
     case AV_STREAM_GROUP_PARAMS_LCEVC:
         ret = &lcevc_class;
         break;
+    case AV_STREAM_GROUP_PARAMS_TMCD:
+        ret = &tmcd_class;
+        break;
     default:
         break;
     }
@@ -484,6 +503,13 @@ AVStreamGroup 
*avformat_stream_group_create(AVFormatContext *s,
         stg->params.lcevc->av_class = &lcevc_class;
         av_opt_set_defaults(stg->params.lcevc);
         break;
+    case AV_STREAM_GROUP_PARAMS_TMCD:
+        stg->params.tmcd = av_mallocz(sizeof(*stg->params.tmcd));
+        if (!stg->params.tmcd)
+            goto fail;
+        stg->params.tmcd->av_class = &tmcd_class;
+        av_opt_set_defaults(stg->params.tmcd);
+        break;
     default:
         goto fail;
     }
-- 
2.52.0


>From ef2433ffb4818c822e8552328e2f7f0dc161e78f Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Tue, 17 Mar 2026 09:33:49 -0300
Subject: [PATCH 3/4] avformat/mov: handle all references in a tmcd box

tref types can have more than one value, as is the case of tmcd in
fcp_export8-236.mov, where the single video track references all timecode
tracks.

Signed-off-by: James Almer <[email protected]>
---
 libavformat/isom.h                       |  3 +-
 libavformat/mov.c                        | 88 +++++++++++++++++-------
 tests/ref/fate/mov-mp4-with-mov-in24-ver |  5 ++
 3 files changed, 69 insertions(+), 27 deletions(-)

diff --git a/libavformat/isom.h b/libavformat/isom.h
index 070389c665..d62732e9a2 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -224,7 +224,8 @@ typedef struct MOVStreamContext {
     int dref_id;
     int nb_sbas_id;
     uint32_t *sbas_id;
-    int timecode_track;
+    int nb_timecode_track;
+    uint32_t *timecode_track;
     int width;            ///< tkhd width
     int height;           ///< tkhd height
     int h_spacing;        ///< pasp hSpacing
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 75b23481cd..339307ed56 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -6401,11 +6401,23 @@ static int mov_read_elst(MOVContext *c, AVIOContext 
*pb, MOVAtom atom)
 static int mov_read_tmcd(MOVContext *c, AVIOContext *pb, MOVAtom atom)
 {
     MOVStreamContext *sc;
+    int err;
 
     if (c->fc->nb_streams < 1)
         return AVERROR_INVALIDDATA;
     sc = c->fc->streams[c->fc->nb_streams - 1]->priv_data;
-    sc->timecode_track = avio_rb32(pb);
+
+    if (atom.size % sizeof(*sc->timecode_track))
+        return AVERROR_INVALIDDATA;
+
+    err = av_reallocp(&sc->timecode_track, atom.size);
+    if (err < 0)
+        return err;
+
+    sc->nb_timecode_track = atom.size >> 2;
+    for (int i = 0; i < sc->nb_timecode_track; i++)
+        sc->timecode_track[i] = avio_rb32(pb);
+
     return 0;
 }
 
@@ -10090,6 +10102,7 @@ static void mov_free_stream_context(AVFormatContext *s, 
AVStream *st)
     av_freep(&sc->display_matrix);
     av_freep(&sc->index_ranges);
     av_freep(&sc->sbas_id);
+    av_freep(&sc->timecode_track);
 
     if (sc->extradata)
         for (int i = 0; i < sc->stsd_count; i++)
@@ -10177,9 +10190,10 @@ static int tmcd_is_referenced(AVFormatContext *s, int 
tmcd_id)
         AVStream *st = s->streams[i];
         MOVStreamContext *sc = st->priv_data;
 
-        if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
-            sc->timecode_track == tmcd_id)
-            return 1;
+        for (int j = 0; j < sc->nb_timecode_track; j++)
+            if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
+                sc->timecode_track[j] == tmcd_id)
+                return 1;
     }
     return 0;
 }
@@ -10193,7 +10207,7 @@ static void export_orphan_timecode(AVFormatContext *s)
         AVStream *st = s->streams[i];
 
         if (st->codecpar->codec_tag  == MKTAG('t','m','c','d') &&
-            !tmcd_is_referenced(s, i + 1)) {
+            !tmcd_is_referenced(s, st->id)) {
             AVDictionaryEntry *tcr = av_dict_get(st->metadata, "timecode", 
NULL, 0);
             if (tcr) {
                 av_dict_set(&s->metadata, "timecode", tcr->value, 0);
@@ -10785,16 +10799,14 @@ static int mov_parse_heif_items(AVFormatContext *s)
 }
 
 static AVStream *mov_find_reference_track(AVFormatContext *s, AVStream *st,
-                                          int first_index)
+                                          uint32_t *tref_id, int nb_tref_id, 
int first_index)
 {
-    MOVStreamContext *sc = st->priv_data;
-
-    if (!sc->nb_sbas_id)
+    if (!nb_tref_id)
         return NULL;
 
     for (int i = first_index; i < s->nb_streams; i++)
-        for (int j = 0; j < sc->nb_sbas_id; j++)
-            if (s->streams[i]->id == sc->sbas_id[j])
+        for (int j = 0; j < nb_tref_id; j++)
+            if (s->streams[i]->id == tref_id[j])
                 return s->streams[i];
 
     return NULL;
@@ -10823,7 +10835,7 @@ static int mov_parse_lcevc_streams(AVFormatContext *s)
         stg->params.lcevc->width  = st->codecpar->width;
         stg->params.lcevc->height = st->codecpar->height;
 
-        while (st_base = mov_find_reference_track(s, st, j)) {
+        while (st_base = mov_find_reference_track(s, st, sc->sbas_id, 
sc->nb_sbas_id, j)) {
             err = avformat_stream_group_add_stream(stg, st_base);
             if (err < 0)
                 return err;
@@ -10940,24 +10952,48 @@ static int mov_read_header(AVFormatContext *s)
 
     /* copy timecode metadata from tmcd tracks to the related video streams */
     for (i = 0; i < s->nb_streams; i++) {
-        AVStream *st = s->streams[i];
-        MOVStreamContext *sc = st->priv_data;
-        if (sc->timecode_track > 0) {
-            AVDictionaryEntry *tcr;
-            int tmcd_st_id = -1;
+        AVStreamGroup *stg;
+        const AVDictionaryEntry *tcr;
+        AVStream *st2, *st = s->streams[i];
 
-            for (j = 0; j < s->nb_streams; j++) {
-                MOVStreamContext *sc2 = s->streams[j]->priv_data;
-                if (sc2->id == sc->timecode_track)
-                    tmcd_st_id = j;
-            }
+        if (st->codecpar->codec_tag != MKTAG('t','m','c','d'))
+            continue;
 
-            if (tmcd_st_id < 0 || tmcd_st_id == i)
+        stg = avformat_stream_group_create(s, AV_STREAM_GROUP_PARAMS_TMCD, 
NULL);
+        if (!stg)
+            return AVERROR(ENOMEM);
+
+        stg->id = st->id;
+        tcr = av_dict_get(st->metadata, "timecode", NULL, 0);
+
+        for (int j = 0; j < s->nb_streams; j++) {
+            AVStream *st2 = s->streams[j];
+            MOVStreamContext *sc2 = st2->priv_data;
+
+            if (!sc2->nb_timecode_track)
                 continue;
-            tcr = av_dict_get(s->streams[tmcd_st_id]->metadata, "timecode", 
NULL, 0);
-            if (tcr)
-                av_dict_set(&st->metadata, "timecode", tcr->value, 0);
+
+            for (int k = 0; k < sc2->nb_timecode_track; k++) {
+                if (sc2->timecode_track[k] != st->id)
+                    continue;
+
+                err = avformat_stream_group_add_stream(stg, st2);
+                if (err < 0)
+                    return err;
+
+                if (tcr)
+                    av_dict_set(&st2->metadata, "timecode", tcr->value, 
AV_DICT_DONT_OVERWRITE);
+            }
         }
+
+        if (!stg->nb_streams) {
+            ff_remove_stream_group(s, stg);
+            continue;
+        }
+
+        err = avformat_stream_group_add_stream(stg, st);
+        if (err < 0)
+            return err;
     }
     export_orphan_timecode(s);
 
diff --git a/tests/ref/fate/mov-mp4-with-mov-in24-ver 
b/tests/ref/fate/mov-mp4-with-mov-in24-ver
index b5522454df..450560b149 100644
--- a/tests/ref/fate/mov-mp4-with-mov-in24-ver
+++ b/tests/ref/fate/mov-mp4-with-mov-in24-ver
@@ -1,3 +1,8 @@
+[STREAM_GROUP]
+[STREAM]
+codec_name=pcm_s24le
+[/STREAM]
+[/STREAM_GROUP]
 [STREAM]
 codec_name=pcm_s24le
 [/STREAM]
-- 
2.52.0


>From b3e24902aa01b3e21e9a2cf5398bd77556868f68 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Tue, 17 Mar 2026 18:13:23 -0300
Subject: [PATCH 4/4] avformat/movenc: support writing more than one entry per
 tref tag

Signed-off-by: James Almer <[email protected]>
---
 libavformat/movenc.c        | 214 ++++++++++++++++++++++++++++++------
 libavformat/movenc.h        |  13 ++-
 libavformat/movenchint.c    |   6 +-
 tests/ref/fate/copy-trac236 |   4 +-
 4 files changed, 200 insertions(+), 37 deletions(-)

diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 570dc1efef..f39f794860 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -3837,15 +3837,15 @@ static int mov_write_minf_tag(AVFormatContext *s, 
AVIOContext *pb, MOVMuxContext
 static void get_pts_range(MOVMuxContext *mov, MOVTrack *track,
                           int64_t *start, int64_t *end)
 {
-    if (track->tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd) {
+    if (track->tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd && 
track->nb_src_track) {
         // tmcd tracks gets track_duration set in mov_write_moov_tag from
         // another track's duration, while the end_pts may be left at zero.
         // Calculate the pts duration for that track instead.
-        get_pts_range(mov, &mov->tracks[track->src_track], start, end);
+        get_pts_range(mov, &mov->tracks[*track->src_track], start, end);
         *start = av_rescale(*start, track->timescale,
-                            mov->tracks[track->src_track].timescale);
+                            mov->tracks[*track->src_track].timescale);
         *end   = av_rescale(*end, track->timescale,
-                            mov->tracks[track->src_track].timescale);
+                            mov->tracks[*track->src_track].timescale);
         return;
     }
     if (track->end_pts != AV_NOPTS_VALUE &&
@@ -4212,12 +4212,18 @@ static int mov_write_edts_tag(AVIOContext *pb, 
MOVMuxContext *mov,
 
 static int mov_write_tref_tag(AVIOContext *pb, MOVTrack *track)
 {
-    avio_wb32(pb, 20);   // size
+    int64_t pos = avio_tell(pb);
+    avio_wb32(pb, 0);   // size placeholder
     ffio_wfourcc(pb, "tref");
-    avio_wb32(pb, 12);   // size (subatom)
-    avio_wl32(pb, track->tref_tag);
-    avio_wb32(pb, track->tref_id);
-    return 20;
+
+    for (int i = 0; i < track->nb_tref_tags; i++) {
+        MovTag *tag = &track->tref_tags[i];
+        avio_wb32(pb, 8 + tag->nb_id * sizeof(*tag->id)); // size (subatom)
+        avio_wl32(pb, tag->name);
+        for (int j = 0; j < tag->nb_id; j++)
+            avio_wb32(pb, tag->id[j]);
+    }
+    return update_size(pb, pos);
 }
 
 // goes at the end of each track!  ... Critical for PSP playback 
("Incompatible data" without it)
@@ -4245,7 +4251,8 @@ static int mov_write_udta_sdp(AVIOContext *pb, MOVTrack 
*track)
     char buf[1000] = "";
     int len;
 
-    ff_sdp_write_media(buf, sizeof(buf), ctx->streams[0], track->src_track,
+    av_assert0(track->nb_src_track);
+    ff_sdp_write_media(buf, sizeof(buf), ctx->streams[0], *track->src_track,
                        NULL, NULL, 0, 0, ctx);
     av_strlcatf(buf, sizeof(buf), "a=control:streamid=%d\r\n", 
track->track_id);
     len = strlen(buf);
@@ -4382,7 +4389,7 @@ static int mov_write_trak_tag(AVFormatContext *s, 
AVIOContext *pb, MOVMuxContext
     if (mov->is_animated_avif)
         mov_write_edts_tag(pb, mov, track);
 
-    if (track->tref_tag)
+    if (track->nb_tref_tags)
         mov_write_tref_tag(pb, track);
 
     if ((ret = mov_write_mdia_tag(s, pb, mov, track)) < 0)
@@ -5208,6 +5215,58 @@ static int mov_setup_track_ids(MOVMuxContext *mov, 
AVFormatContext *s)
     return 0;
 }
 
+static int mov_find_tref_id(MOVMuxContext *mov, const MovTag *tag, uint32_t id)
+{
+    for (int i = 0; i < tag->nb_id; i++) {
+        if (tag->id[i] == id)
+            return 1;
+    }
+    return 0;
+}
+
+static int mov_add_tref_id(MOVMuxContext *mov, MovTag *tag, uint32_t id)
+{
+    int ret = mov_find_tref_id(mov, tag, id);
+
+    if (!ret) {
+        uint32_t *tmp = av_realloc_array(tag->id, tag->nb_id + 1, 
sizeof(*tag->id));
+        if (!tmp)
+            return AVERROR(ENOMEM);
+        tag->id = tmp;
+        tag->id[tag->nb_id++] = id;
+    }
+
+    return 0;
+}
+
+static MovTag *mov_find_tref_tag(MOVMuxContext *mov, const MOVTrack *trk, 
uint32_t name)
+{
+    for (int i = 0; i < trk->nb_tref_tags; i++) {
+        MovTag *entry = &trk->tref_tags[i];
+
+        if (entry->name == name)
+            return entry;
+    }
+    return NULL;
+}
+
+static MovTag *mov_add_tref_tag(MOVMuxContext *mov, MOVTrack *trk, uint32_t 
name)
+{
+    MovTag *tag = mov_find_tref_tag(mov, trk, name);
+
+    if (!tag) {
+        MovTag *tmp = av_realloc_array(trk->tref_tags, trk->nb_tref_tags + 1,
+                                       sizeof(*trk->tref_tags));
+        if (!tmp)
+            return NULL;
+        trk->tref_tags = tmp;
+        tag = &trk->tref_tags[trk->nb_tref_tags++];
+        *tag = (MovTag){ .name = name };
+    }
+
+    return tag;
+}
+
 static int mov_write_moov_tag(AVIOContext *pb, MOVMuxContext *mov,
                               AVFormatContext *s)
 {
@@ -5230,17 +5289,33 @@ static int mov_write_moov_tag(AVIOContext *pb, 
MOVMuxContext *mov,
 
     if (mov->chapter_track)
         for (i = 0; i < mov->nb_streams; i++) {
-            mov->tracks[i].tref_tag = MKTAG('c','h','a','p');
-            mov->tracks[i].tref_id  = mov->tracks[mov->chapter_track].track_id;
+            MovTag *tag = mov_add_tref_tag(mov, &mov->tracks[i], 
MKTAG('c','h','a','p'));
+            if (!tag)
+                return AVERROR(ENOMEM);
+
+            int ret = mov_add_tref_id(mov, tag, 
mov->tracks[mov->chapter_track].track_id);
+            if (ret < 0)
+                return ret;
         }
     for (i = 0; i < mov->nb_tracks; i++) {
         MOVTrack *track = &mov->tracks[i];
         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;
+            MovTag *tag = mov_add_tref_tag(mov, track, MKTAG('h','i','n','t'));
+            if (!tag)
+                return AVERROR(ENOMEM);
+
+            av_assert0(track->nb_src_track);
+            int ret = mov_add_tref_id(mov, tag, 
mov->tracks[*track->src_track].track_id);
+            if (ret < 0)
+                return ret;
+        } else if (track->tag == MKTAG('l','v','c','1') && 
track->nb_src_track) {
+            MovTag *tag = mov_add_tref_tag(mov, track, MKTAG('s','b','a','s'));
+            if (!tag)
+                return AVERROR(ENOMEM);
+
+            int ret = mov_add_tref_id(mov, tag, 
mov->tracks[*track->src_track].track_id);
+            if (ret < 0)
+                return ret;
         } 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,
@@ -5248,17 +5323,28 @@ static int mov_write_moov_tag(AVIOContext *pb, 
MOVMuxContext *mov,
             if (sd && sd->size == sizeof(int)) {
                 int *fallback = (int *)sd->data;
                 if (*fallback >= 0 && *fallback < mov->nb_tracks) {
-                    track->tref_tag = MKTAG('f','a','l','l');
-                    track->tref_id = mov->tracks[*fallback].track_id;
+                    MovTag *tag = mov_add_tref_tag(mov, track, 
MKTAG('f','a','l','l'));
+                    if (!tag)
+                        return AVERROR(ENOMEM);
+
+                    int ret = mov_add_tref_id(mov, tag, 
mov->tracks[*fallback].track_id);
+                    if (ret < 0)
+                        return ret;
                 }
             }
         }
     }
     for (i = 0; i < mov->nb_tracks; i++) {
-        if (mov->tracks[i].tag == MKTAG('t','m','c','d')) {
-            int src_trk = mov->tracks[i].src_track;
-            mov->tracks[src_trk].tref_tag = mov->tracks[i].tag;
-            mov->tracks[src_trk].tref_id  = mov->tracks[i].track_id;
+        if (mov->tracks[i].tag != MKTAG('t','m','c','d'))
+            continue;
+        for (int j = 0; j < mov->tracks[i].nb_src_track; j++) {
+            int src_trk = mov->tracks[i].src_track[j];
+            MovTag *tag = mov_add_tref_tag(mov, &mov->tracks[src_trk], 
mov->tracks[i].tag);
+            if (!tag)
+                return AVERROR(ENOMEM);
+            int ret = mov_add_tref_id(mov, tag, mov->tracks[i].track_id);
+            if (ret < 0)
+                return ret;
             //src_trk may have a different timescale than the tmcd track
             mov->tracks[i].track_duration = 
av_rescale(mov->tracks[src_trk].track_duration,
                                                        
mov->tracks[i].timescale,
@@ -7661,7 +7747,11 @@ static int mov_create_timecode_track(AVFormatContext *s, 
int index, int src_inde
     /* tmcd track based on video stream */
     track->mode      = mov->mode;
     track->tag       = MKTAG('t','m','c','d');
-    track->src_track = src_index;
+    track->src_track = av_malloc(sizeof(*track->src_track));
+    if (!track->src_track)
+        return AVERROR(ENOMEM);
+    *track->src_track = src_index;
+    track->nb_src_track = 1;
     track->timescale = mov->tracks[src_index].timescale;
     if (tc.flags & AV_TIMECODE_FLAG_DROPFRAME)
         track->timecode_flags |= MOV_TIMECODE_FLAG_DROPFRAME;
@@ -7778,6 +7868,11 @@ static void mov_free(AVFormatContext *s)
         av_freep(&track->extradata);
         av_freep(&track->extradata_size);
 
+        for (int i = 0; i < track->nb_tref_tags; i++)
+            av_freep(&track->tref_tags[i].id);
+        av_freep(&track->tref_tags);
+        av_freep(&track->src_track);
+
         ff_mov_cenc_free(&track->cenc);
         ffio_free_dyn_buf(&track->mdat_buf);
 
@@ -8435,9 +8530,8 @@ 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;
-
+        switch (stg->type) {
+        case AV_STREAM_GROUP_PARAMS_LCEVC: {
         AVStreamGroupLCEVC *lcevc = stg->params.lcevc;
         AVStream *st    = stg->streams[lcevc->lcevc_index];
         MOVTrack *track = st->priv_data;
@@ -8446,13 +8540,66 @@ static int mov_init(AVFormatContext *s)
             MOVTrack *trk = &mov->tracks[j];
 
             if (trk->st == stg->streams[!lcevc->lcevc_index]) {
-                track->src_track = j;
+                track->src_track = av_malloc(sizeof(*track->src_track));
+                if (!track->src_track)
+                    return AVERROR(ENOMEM);
+                *track->src_track = j;
+                track->nb_src_track = 1;
                 break;
             }
         }
 
         track->par->width = lcevc->width;
         track->par->height = track->height = lcevc->height;
+        break;
+        }
+        case AV_STREAM_GROUP_PARAMS_TMCD: {
+            AVStreamGroupTMCD *tmcd = stg->params.tmcd;
+            AVStream *st = stg->streams[tmcd->tmcd_index];
+            MOVTrack *track = st->priv_data;
+
+            for (int j = 0; j < stg->nb_streams; j++) {
+                AVStream *st2 = stg->streams[j];
+                int index = -1;
+
+                for (int k = 0; k < mov->nb_tracks; k++) {
+                    if (mov->tracks[k].st != st2)
+                        continue;
+                    index = k;
+                    break;
+                }
+                if (index < 0)
+                    return AVERROR(EINVAL);
+
+                int *tmp = av_realloc(track->src_track,
+                                      (track->nb_src_track + 1) * 
sizeof(*track->src_track));
+                if (!tmp)
+                    return AVERROR(ENOMEM);
+                track->src_track = tmp;
+                track->src_track[track->nb_src_track++] = index;
+            }
+            break;
+        }
+        default:
+            break;
+        }
+    }
+
+    /* fill src_track for tmcd tracks not covered by stream groups or
+     * created by this muxer, retaining the old behavior of src_track
+     * being arbitrarily 0. */
+    for (i = 0; i < s->nb_streams; i++) {
+        AVStream *st = s->streams[i];
+        MOVTrack *track = st->priv_data;
+        if (st->codecpar->codec_tag != MKTAG('t','m','c','d') ||
+            track->nb_src_track)
+            continue;
+
+        track->src_track = av_malloc(sizeof(*track->src_track));
+        if (!track->src_track)
+            return AVERROR(ENOMEM);
+        *track->src_track = 0;
+        track->nb_src_track = 1;
     }
 
     enable_tracks(s);
@@ -8902,8 +9049,13 @@ static int avif_write_trailer(AVFormatContext *s)
     if (mov->is_animated_avif && mov->nb_streams > 1) {
         // For animated avif with alpha channel, we need to write a tref tag
         // with type "auxl".
-        mov->tracks[1].tref_tag = MKTAG('a', 'u', 'x', 'l');
-        mov->tracks[1].tref_id = 1;
+        MovTag *tag = mov_add_tref_tag(mov, &mov->tracks[1], MKTAG('a', 'u', 
'x', 'l'));
+        if (!tag)
+            return AVERROR(ENOMEM);
+
+        int ret = mov_add_tref_id(mov, tag, 1);
+        if (ret < 0)
+            return ret;
     }
     mov_write_identification(pb, s);
     mov_write_meta_tag(pb, mov, s);
diff --git a/libavformat/movenc.h b/libavformat/movenc.h
index eb12551ee5..ac1a923fb2 100644
--- a/libavformat/movenc.h
+++ b/libavformat/movenc.h
@@ -84,6 +84,12 @@ typedef struct MOVFragmentInfo {
     int size;
 } MOVFragmentInfo;
 
+typedef struct MovTag {
+    uint32_t    name;
+    int         nb_id;
+    uint32_t   *id; ///< trackID of the referenced track
+} MovTag;
+
 typedef struct MOVTrack {
     int         mode;
     int         entry_version;
@@ -125,8 +131,8 @@ typedef struct MOVTrack {
     unsigned    cluster_capacity;
     int         audio_vbr;
     int         height; ///< active picture (w/o VBI) height for D-10/IMX
-    uint32_t    tref_tag;
-    int         tref_id; ///< trackID of the referenced track
+    int         nb_tref_tags;
+    MovTag      *tref_tags;
     int64_t     start_dts;
     int64_t     start_cts;
     int64_t     end_pts;
@@ -134,7 +140,8 @@ typedef struct MOVTrack {
     int64_t     dts_shift;
 
     int         hint_track;   ///< the track that hints this track, -1 if no 
hint track is set
-    int         src_track;    ///< the track that this hint (or tmcd) track 
describes
+    int         nb_src_track;
+    int         *src_track;   ///< the tracks that this hint (or tmcd) track 
describes
     AVFormatContext *rtp_ctx; ///< the format context for the hinting rtp muxer
     uint32_t    prev_rtp_ts;
     int64_t     cur_rtp_ts_unwrapped;
diff --git a/libavformat/movenchint.c b/libavformat/movenchint.c
index 1fa22fbffb..65b3908938 100644
--- a/libavformat/movenchint.c
+++ b/libavformat/movenchint.c
@@ -36,7 +36,11 @@ int ff_mov_init_hinting(AVFormatContext *s, int index, int 
src_index)
     int ret = AVERROR(ENOMEM);
 
     track->tag = MKTAG('r','t','p',' ');
-    track->src_track = src_index;
+    track->src_track = av_malloc(sizeof(*track->src_track));
+    if (!track->src_track)
+        return AVERROR(ENOMEM);
+    *track->src_track = src_index;
+    track->nb_src_track = 1;
 
     track->par = avcodec_parameters_alloc();
     if (!track->par)
diff --git a/tests/ref/fate/copy-trac236 b/tests/ref/fate/copy-trac236
index 1f20e9ea7e..a4920c9913 100644
--- a/tests/ref/fate/copy-trac236
+++ b/tests/ref/fate/copy-trac236
@@ -1,5 +1,5 @@
-3e3497985d54991e09f82ca3dd7eda79 *tests/data/fate/copy-trac236.mov
-630910 tests/data/fate/copy-trac236.mov
+cd72fa882e38a18d48cf139b66097b9c *tests/data/fate/copy-trac236.mov
+630918 tests/data/fate/copy-trac236.mov
 #tb 0: 100/2997
 #media_type 0: video
 #codec_id 0: rawvideo
-- 
2.52.0

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

Reply via email to