PR #25812 opened by luoye-cpu
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25812
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25812.patch

## Summary

`mkv_write_block()` writes three kinds of ITU-T T.35 payload -- HDR10+ 
(CTA-861-H),
SMPTE 2094-40 application 5, and LCEVC, which is carried inside a T.35 frame 
with the
UK country code and the V-Nova provider code -- and all of them went out under
`MATROSKA_BLOCK_ADD_ID_ITU_T_T35` (4). A packet that carries more than one of 
them
therefore produced a `BlockAdditions` with a repeated `BlockAddID`, which RFC 
9559,
5.1.3.5.2.3 forbids: "Each BlockAddID value MUST be unique between all BlockMore
elements found in a BlockAdditions element".  The ID is what identifies the 
family, so
the payloads in such a Block cannot be told apart.

This hands out one `BlockAddID` per kind of T.35 payload and describes each of 
them
with a `BlockAdditionMapping` of its own.

## Root cause

Two things together:

1. `libavformat/matroskaenc.c` hard-coded `MATROSKA_BLOCK_ADD_ID_ITU_T_T35` at 
the
   three places that write a T.35 payload, so `MaxBlockAdditionID` stayed at 4.
2. The TrackEntry got a mapping only when `max_blockaddid == 4`, that is one 
mapping
   for all T.35 data.  Matroska used to require `BlockAddIDValue 4` for every 
T.35
   mapping, which ruled out a second one.

The second half was relaxed by 773bcbe in the specification ("codec: allow more 
than
one BlockAdditionMapping with T.35 data",
ietf-wg-cellar/matroska-specification#1111): with more than one such mapping, 
each one
MUST carry a `BlockAddIDExtraData` holding every byte of the T.35 header -- 
country
code, the optional country code extension and the provider code -- and one of 
them
SHOULD keep the value 4, preferably the HDR10+ one, because readers that predate
`BlockAddIDType` read 4 as HDR10+.

Nothing is needed on the demuxing side: 49fb9b44d5 taught `matroskadec` to 
resolve a
`BlockAddID` through the mapping table, and it looks the value up before the 
type.

## What the change does

- One `BlockAddID` per kind, handed out in the order the kinds are first seen 
on a
  track: 4 for the first one, 5 for the next, 6 for the last.  A kind that 
recurs
  keeps the value it was given, so there is one ID per kind, not one per 
packet; the
  state is per track.
- The TrackEntry reserves room for three mappings, 48 bytes more than before, 
and
  writes the ones the track used when it is finished; what is left over becomes 
a Void
  element.  A Dolby Vision mapping that another part of the file writes behind 
that
  area is not touched.
- The mappings are written from the payloads that were actually seen rather 
than from
  a comparison of `MaxBlockAdditionID` with 4, and `MaxBlockAdditionID` follows 
the
  assigned values, so it grows to 5 and 6 along with them.
- A track that carries a single kind keeps exactly what it had: one mapping, 
value 4,
  no extra data.  Matroska requires the extra data only once there is more than 
one
  T.35 mapping and says nothing about a single one, so leaving it out there 
keeps such
  tracks byte for byte what they were.

The allocation goes by first appearance, not by kind.  HDR10+ is written before 
the
other two, so it gets 4 whenever it is already on the packet that brings the 
track's
first T.35 payload; a track that shows another kind first gives 4 to that kind 
and
writes HDR10+ under 5.  The "preferably the HDR10+ one" part of the 
recommendation is
therefore not followed for such a track: the ID cannot be changed once the 
Block is
written, and holding packets back until an HDR10+ payload shows up is not worth 
it.
What the track does get is a mapping table that agrees with the payloads, which 
is what
readers that know `BlockAddIDType` use.

## Testing

`tests/api/api-matroska-t35-test.c` (new; `make fate-api-matroska-t35`, part of 
the
default `make fate` set) builds packets with `av_packet_new_side_data()`, so no 
sample
is needed, sends them through the muxer into a memory `AVIOContext` -- once 
seekable,
once without a seek function -- and walks the EBML that comes out.  Over seven 
cases it
checks that the `BlockAddID`s inside one `BlockAdditions` are unique, that 
every ID has
a mapping, that the mapping's extra data is byte for byte the T.35 header of the
payload written under it, and what `MaxBlockAdditionID` ends up as.  One case 
brings
the kinds one at a time across four packets, which is where the first-seen order
matters.

Walking the bytes is deliberate: `matroskadec` sniffs the T.35 payload itself, 
so it
hands back side data for every payload even from a file that repeats 
`BlockAddID 4` and
cannot notice the inconsistency.  Against an unpatched tree the same test fails 
with
exactly that.

## Known limitations

- **Output that cannot be seeked is unchanged, and still does not conform.**  
Which
  kinds a track carries is only known once its packets have been seen, and by 
then the
  TrackEntry has been written with no way back to it.  Such output keeps giving 
every
  payload the value 4, as it did before: as soon as a track carries more than 
one kind
  that repeats a `BlockAddID` inside one `BlockAdditions`, which 5.1.3.5.2.3 
forbids.
  Handing out 5 and 6 there instead would only cost payloads, since 
`matroskadec` skips
  a `BlockMore` whose `BlockAddID` has no mapping, so this change does not make 
that
  output worse, but it does not fix it either.  All supported T.35 payloads are
  therefore distinguishable through their mappings only in seekable output.  
Making the
  streamed case work needs the mappings to be writable after the fact, which is 
a
  separate change.
- No Matroska implementation other than `matroskadec` was tried.



>From 019aabed9cd63d1a7cf38c2042b2cda646137df9 Mon Sep 17 00:00:00 2001
From: luoye-cpu <[email protected]>
Date: Fri, 6 Nov 2026 23:07:01 +0800
Subject: [PATCH] avformat/matroskaenc: support multiple T.35 BlockAddIDs

BlockAddIDs have to be unique inside one BlockAdditions element: RFC 9559,
5.1.3.5.2.3 says "Each BlockAddID value MUST be unique between all BlockMore
elements found in a BlockAdditions element".  The ITU-T T.35 payloads
mkv_write_block() writes -- HDR10+ (CTA-861-H), SMPTE 2094-40 application 5,
and LCEVC, which is carried inside a T.35 frame with the UK country code and
the V-Nova provider code -- all went out under the same value,
MATROSKA_BLOCK_ADD_ID_ITU_T_T35 (4).  A packet carrying more than one of them
therefore produced a Block whose BlockAddID appeared more than once, which the
format does not allow: the ID identifies the family, so the payloads cannot be
told apart.

T.35 data of different kinds cannot be merged into one BlockAdditional either,
so each kind needs a BlockAdditionMapping of its own.  Matroska used to require
BlockAddIDValue 4 for every T.35 mapping, which ruled that out.  Commit 773bcbe
in the specification ("codec: allow more than one BlockAdditionMapping with
T.35 data", ietf-wg-cellar/matroska-specification#1111) relaxed this: with
more than one such mapping, each one MUST carry a BlockAddIDExtraData holding
every byte of the T.35 header -- the country code, the optional country code
extension and the provider code -- and one of them SHOULD keep the value 4,
preferably the HDR10+ one, because readers that predate BlockAddIDType take
that value to mean HDR10+.  matroskadec was taught to read such files in
49fb9b44d5 and resolves a BlockAddID through the mapping table, so nothing is
needed on the demuxing side.

Hand the values out in the order the kinds are first seen on a track: the first
kind of T.35 payload a track carries keeps the value 4, every further kind gets
the next value.  A kind that recurs keeps the value it was given, so there is
one ID per kind, not one per packet.  (Only output that can be seeked has the
mappings this needs; what a pipe gets is described at the end.)  A track that
only ever carries one kind keeps the mapping it has today, value 4 and no extra
data.  The extra data is left out there as a choice, to keep such tracks byte
for byte what they were; Matroska requires it only once there is more than one
T.35 mapping, and says nothing about a single one.  One mapping of a track
therefore always uses the value 4, as Matroska asks, and HDR10+ is written
before the other two, so it gets that value whenever it is already on the
packet that brings the first T.35 payload of the track.  A track that shows
another kind first hands the 4 to that kind and writes HDR10+ under 5.  The
allocation goes by first appearance, not by kind, so the recommendation to give
the 4 to the HDR10+ one is not followed for such a track: the ID cannot be
changed once the Block is written, and holding packets back until an HDR10+
payload shows up is not worth it.  What the track still gets is a mapping table
that agrees with the payloads, which is what readers that know BlockAddIDType
use.

The TrackEntry reserves room for the three mappings, 48 bytes more than before,
and the ones the track used are written when it is finished; what is left over
becomes a Void element.  The mappings are written from the T.35 payloads that
were actually seen rather than from a comparison of the maximum BlockAddID with
4, and MaxBlockAdditionID is written from the assigned values, so it grows to 5
and 6 along with them.

Checked with packets built through av_packet_new_side_data(), so that no sample
is needed: tests/api/api-matroska-t35-test.c sends them through the muxer into
a memory AVIOContext, once seekable and once without a seek function, and walks
the EBML that comes out to look at the BlockAdditions, the mapping table and
MaxBlockAdditionID.  Walking the bytes is what matroskadec cannot do here: it
sniffs the T.35 payload itself, so it hands back side data for every payload
even from a file that repeats BlockAddID 4 and would not notice the problem.
The walk also compares each mapping's extra data byte for byte with the T.35
header of the payload written under it, which an element-is-present test would
miss.  With HDR10+ and SMPTE 2094-40 application 5 on one packet the muxer
writes BlockAddIDs 4 and 5, MaxBlockAdditionID is 5, and each mapping carries
the header of its own payload; with all three kinds the values are 4, 5 and 6;
a track with a single kind keeps one mapping with the value 4 and no extra
data.  One case brings the kinds one at a time instead, across four packets --
application 5, then HDR10+, then LCEVC, then application 5 and HDR10+ again --
which is where the order matters: the values 4, 5 and 6 are handed out by first
appearance and kept when a kind comes back, and MaxBlockAdditionID ends at 6.
Without the change those same packets get BlockMores that all carry BlockAddID
4, and the test reports exactly that.  The reserved area is written back in
place, so a Dolby Vision mapping that another part of the file put behind it is
not touched, and a track that carries no T.35 payload keeps the area as a Void.

Output that cannot be seeked has nowhere to put these mappings: which kinds a
track carries is only known once its packets have been seen, and by then the
TrackEntry has been written with no way back to it.  Such output keeps giving
every payload the value 4, as it did before this change.  That is a limitation
of the muxer and not a shape the format allows: as soon as a track carries more
than one kind it repeats a BlockAddID inside one BlockAdditions, which
5.1.3.5.2.3 forbids, and a reader that resolves BlockAddIDs through the
mapping table has nothing to resolve them with.  Handing out 5 and 6 there
instead would only cost payloads -- matroskadec skips a BlockMore whose
BlockAddID has no mapping -- without telling any reader more than the repeated
value does, so this change does not make that output worse, but it does not fix
it either.  All supported T.35 payloads are therefore distinguishable through
their mappings only in seekable output; in output that cannot be seeked they
are still written out, but the file does not say which kind each of them is.
No Matroska implementation other than matroskadec was tried.

Signed-off-by: luoye-cpu <[email protected]>
---
 libavformat/matroskaenc.c         | 155 ++++++-
 tests/api/Makefile                |   1 +
 tests/api/api-matroska-t35-test.c | 716 ++++++++++++++++++++++++++++++
 tests/fate/api.mak                |   5 +
 4 files changed, 855 insertions(+), 22 deletions(-)
 create mode 100644 tests/api/api-matroska-t35-test.c

diff --git a/libavformat/matroskaenc.c b/libavformat/matroskaenc.c
index a8408da8c1a..fb136d23cfa 100644
--- a/libavformat/matroskaenc.c
+++ b/libavformat/matroskaenc.c
@@ -185,6 +185,40 @@ typedef struct mkv_cues {
 
 struct MatroskaMuxContext;
 
+/* The ITU-T T.35 payloads mkv_write_block() can write, in the order it checks
+ * them.  Each one needs a BlockAdditionMapping of its own: T.35 data of
+ * different kinds cannot be told apart when they share a BlockAddID. */
+enum {
+    T35_HDR10_PLUS,
+    T35_SMPTE2094_APP5,
+    T35_LCEVC,
+    T35_NB,
+};
+
+/* The first bytes of the T.35 header each of them is written with: country
+ * code, optional country code extension, terminal provider code and terminal
+ * provider oriented code.  Matroska requires them as BlockAddIDExtraData when
+ * more than one T.35 BlockAdditionMapping is used. */
+static const struct {
+    uint8_t size;
+    uint8_t data[5];
+} t35_header[T35_NB] = {
+    { 5, { ITU_T_T35_COUNTRY_CODE_US,    ITU_T_T35_PROVIDER_CODE_SAMSUNG >> 8,
+           ITU_T_T35_PROVIDER_CODE_SAMSUNG & 0xFF, 0x00, 0x01 } },
+    { 5, { ITU_T_T35_COUNTRY_CODE_US,    ITU_T_T35_PROVIDER_CODE_SMPTE >> 8,
+           ITU_T_T35_PROVIDER_CODE_SMPTE & 0xFF,   0x00, 0x01 } },
+    { 4, { ITU_T_T35_COUNTRY_CODE_UK,    0x00,
+           ITU_T_T35_PROVIDER_CODE_VNOVA >> 8,
+           ITU_T_T35_PROVIDER_CODE_VNOVA & 0xFF } },
+};
+
+/* Size of one BlockAdditionMapping element: element ID and size (3), the
+ * BlockAddIDType and BlockAddIDValue integers (4 each) and a
+ * BlockAddIDExtraData holding the longest header above (2 + 1 + 5).  The
+ * length of the header is taken from the table so that a longer one added
+ * there also enlarges the space reserved for the mappings. */
+#define T35_MAPPING_SIZE (3 + 4 + 4 + 2 + 1 + sizeof(t35_header[0].data))
+
 typedef struct mkv_track {
     int             write_dts;
     int             has_cue;
@@ -198,7 +232,15 @@ typedef struct mkv_track {
     int64_t         duration;
     int64_t         duration_offset;
     uint64_t        max_blockaddid;
+    /* BlockAddID given to each of the payloads in t35_header, zero while the
+     * track has not carried it yet, and how many of them have been given one.
+     * See mkv_t35_blockaddid(). */
+    uint64_t        t35_blockaddid[T35_NB];
+    unsigned        nb_t35_blockaddids;
     int64_t         blockadditionmapping_offset;
+    /* End of the space reserved for the BlockAdditionMappings, only set when
+     * that space was reserved, i.e. for seekable output. */
+    int64_t         blockadditionmapping_end;
     int             codecpriv_offset;
     unsigned        codecpriv_size;     ///< size reserved for CodecPrivate 
excluding header+length field
     int64_t         ts_offset;
@@ -1742,11 +1784,13 @@ static void 
mkv_write_blockadditionmapping(AVFormatContext *s, const MatroskaMux
         // be overwritten when finishing the track.
         put_ebml_uint(pb, MATROSKA_ID_TRACKMAXBLKADDID, 0);
         if (par->codec_type == AVMEDIA_TYPE_VIDEO) {
-            // Similarly, reserve space for an eventual
-            // HDR10+ ITU T.35 metadata BlockAdditionMapping.
-            put_ebml_void(pb, 3 /* BlockAdditionMapping */
-                            + 4 /* BlockAddIDValue */
-                            + 4 /* BlockAddIDType */);
+            // Similarly, reserve space for the BlockAdditionMappings of the
+            // ITU T.35 metadata the track may end up carrying. Which of them
+            // are actually needed is only known once the packets have been
+            // seen. The two extra bytes make sure what is left can always be
+            // skipped with a Void element, which takes at least two bytes.
+            put_ebml_void(pb, T35_NB * T35_MAPPING_SIZE + 2);
+            track->blockadditionmapping_end = avio_tell(pb);
         }
     }
 
@@ -2860,6 +2904,38 @@ static void mkv_write_blockadditional(EbmlWriter 
*writer, const uint8_t *buf,
     ebml_writer_close_master(writer);
 }
 
+/**
+ * Return the BlockAddID to write a T.35 payload under, assigning one on first
+ * use.  A track that only ever carries one kind of payload keeps the value 4,
+ * which is what it got before and what Matroska asks for when there is a
+ * single T.35 BlockAdditionMapping; every further kind gets the next value,
+ * which keeps the BlockAddIDs inside one BlockAdditions unique as required.
+ *
+ * HDR10+ is written first, so it takes the value 4 whenever it is already
+ * present in the packet that brings the first T.35 payload of the track.  A
+ * track whose first such packet carries another kind assigns that kind 4 and
+ * puts HDR10+ under a higher value; by then the payloads already written
+ * cannot be given another ID.  The specification only prefers, not requires,
+ * that HDR10+ keeps the value 4.
+ *
+ * A value other than 4 only means anything if the BlockAdditionMapping that
+ * translates it back is there, and that can only be written once the payloads
+ * have been seen, i.e. when the TrackEntry can be patched afterwards.  Output
+ * that cannot be seeked has no such place, so it keeps giving every payload
+ * the value 4: an ID nothing resolves is worse than the repeated one, and this
+ * way that output stays what it was.
+ */
+static uint64_t mkv_t35_blockaddid(mkv_track *track, int t35)
+{
+    if (!track->blockadditionmapping_end)
+        return MATROSKA_BLOCK_ADD_ID_ITU_T_T35;
+
+    if (!track->t35_blockaddid[t35])
+        track->t35_blockaddid[t35] = MATROSKA_BLOCK_ADD_ID_ITU_T_T35 +
+                                     track->nb_t35_blockaddids++;
+    return track->t35_blockaddid[t35];
+}
+
 static int mkv_write_block(void *logctx, MatroskaMuxContext *mkv,
                            AVIOContext *pb, const AVStream *st,
                            mkv_track *track, const AVPacket *pkt,
@@ -2874,6 +2950,7 @@ static int mkv_write_block(void *logctx, 
MatroskaMuxContext *mkv,
     uint8_t *side_data;
     size_t side_data_size;
     uint64_t additional_id;
+    uint64_t blockaddid;
     unsigned track_number = track->track_num;
     // BlockGroup, Block, BlockDuration, DiscardPadding, BlockReference
     // and BlockAdditions with three elements per BlockMore
@@ -2961,10 +3038,10 @@ static int mkv_write_block(void *logctx, 
MatroskaMuxContext *mkv,
             if (ret < 0)
                 return ret;
 
+            blockaddid = mkv_t35_blockaddid(track, T35_HDR10_PLUS);
             mkv_write_blockadditional(&writer, t35_buf, payload_size + 6,
-                                      MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
-            track->max_blockaddid = FFMAX(track->max_blockaddid,
-                                          MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
+                                      blockaddid);
+            track->max_blockaddid = FFMAX(track->max_blockaddid, blockaddid);
         }
         side_data = av_packet_get_side_data(pkt,
                                             
AV_PKT_DATA_DYNAMIC_HDR_SMPTE_2094_APP5,
@@ -2982,10 +3059,10 @@ static int mkv_write_block(void *logctx, 
MatroskaMuxContext *mkv,
             if (ret < 0)
                 return ret;
 
+            blockaddid = mkv_t35_blockaddid(track, T35_SMPTE2094_APP5);
             mkv_write_blockadditional(&writer, smpte_2094_app5_buf, 
payload_size + 5,
-                                      MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
-            track->max_blockaddid = FFMAX(track->max_blockaddid,
-                                          MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
+                                      blockaddid);
+            track->max_blockaddid = FFMAX(track->max_blockaddid, blockaddid);
         }
         side_data = av_packet_get_side_data(pkt, AV_PKT_DATA_LCEVC,
                                             &side_data_size);
@@ -3001,10 +3078,9 @@ static int mkv_write_block(void *logctx, 
MatroskaMuxContext *mkv,
             AV_WB16(lcevc + 2, ITU_T_T35_PROVIDER_CODE_VNOVA);
             memcpy (lcevc + 4, side_data, side_data_size);
 
-            mkv_write_blockadditional(&writer, lcevc, payload_size,
-                                      MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
-            track->max_blockaddid = FFMAX(track->max_blockaddid,
-                                          MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
+            blockaddid = mkv_t35_blockaddid(track, T35_LCEVC);
+            mkv_write_blockadditional(&writer, lcevc, payload_size, 
blockaddid);
+            track->max_blockaddid = FFMAX(track->max_blockaddid, blockaddid);
         }
     }
 
@@ -3293,6 +3369,27 @@ static int mkv_write_flush_packet(AVFormatContext *s, 
AVPacket *pkt)
     return mkv_write_packet(s, pkt);
 }
 
+/**
+ * Write the BlockAdditionMapping of one ITU-T T.35 payload.  Matroska requires
+ * the beginning of the T.35 header as BlockAddIDExtraData once there is more
+ * than one such mapping, as that is what tells the payloads apart.  A single
+ * one needs no extra data; leaving it out is this muxer's choice, made so that
+ * a track carrying a single kind keeps the bytes it wrote before.
+ */
+static void mkv_write_t35_mapping(AVIOContext *pb, uint64_t blockaddid,
+                                  const uint8_t *extra, int extra_size)
+{
+    ebml_master mapping = start_ebml_master(pb, MATROSKA_ID_TRACKBLKADDMAPPING,
+                                            8 + (extra ? 3 + extra_size : 0));
+
+    put_ebml_uint(pb, MATROSKA_ID_BLKADDIDTYPE,
+                  MATROSKA_BLOCK_ADD_ID_TYPE_ITU_T_T35);
+    put_ebml_uint(pb, MATROSKA_ID_BLKADDIDVALUE, blockaddid);
+    if (extra)
+        put_ebml_binary(pb, MATROSKA_ID_BLKADDIDEXTRADATA, extra, extra_size);
+    end_ebml_master(pb, mapping);
+}
+
 static int mkv_write_trailer(AVFormatContext *s)
 {
     MatroskaMuxContext *mkv = s->priv_data;
@@ -3436,13 +3533,27 @@ after_cues:
 
             put_ebml_uint(track_bc, MATROSKA_ID_TRACKMAXBLKADDID,
                           track->max_blockaddid);
-            if (track->max_blockaddid == MATROSKA_BLOCK_ADD_ID_ITU_T_T35) {
-                ebml_master mapping_master = start_ebml_master(track_bc, 
MATROSKA_ID_TRACKBLKADDMAPPING, 8);
-                put_ebml_uint(track_bc, MATROSKA_ID_BLKADDIDTYPE,
-                              MATROSKA_BLOCK_ADD_ID_TYPE_ITU_T_T35);
-                put_ebml_uint(track_bc, MATROSKA_ID_BLKADDIDVALUE,
-                              MATROSKA_BLOCK_ADD_ID_ITU_T_T35);
-                end_ebml_master(track_bc, mapping_master);
+            for (int t35 = 0; t35 < T35_NB; t35++) {
+                int extra_size = track->nb_t35_blockaddids > 1 ?
+                                 t35_header[t35].size : 0;
+
+                if (!track->t35_blockaddid[t35])
+                    continue;
+                mkv_write_t35_mapping(track_bc, track->t35_blockaddid[t35],
+                                      extra_size ? t35_header[t35].data : NULL,
+                                      extra_size);
+            }
+            if (track->blockadditionmapping_end) {
+                int64_t end       = track->blockadditionmapping_end;
+                int     remaining = end - avio_tell(track_bc);
+
+                /* What is left of the reservation becomes a Void element,
+                 * which needs at least two bytes.  The reservation holds
+                 * T35_NB mappings of at most T35_MAPPING_SIZE bytes plus
+                 * those two, so what is left is always at least two; this
+                 * also catches a mapping that outgrew what was reserved. */
+                av_assert0(remaining >= 2);
+                put_ebml_void(track_bc, remaining);
             }
         }
 
diff --git a/tests/api/Makefile b/tests/api/Makefile
index c411e82e5f1..d64d473d687 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -4,6 +4,7 @@ APITESTPROGS-$(call ENCDEC, FLAC, FLAC) += api-flac
 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264
 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264-slice
 APITESTPROGS-$(CONFIG_MOV_MUXER)        += api-movenc
+APITESTPROGS-$(CONFIG_MATROSKA_MUXER)   += api-matroska-t35
 APITESTPROGS-$(CONFIG_MOVTEXT_ENCODER)  += api-movtext
 APITESTPROGS-$(call ALLYES, HEVC_MP4TOANNEXB_BSF HEVC_DECODER) += 
api-hevc-mp4toannexb
 APITESTPROGS-yes += api-seek api-dump-stream-meta
diff --git a/tests/api/api-matroska-t35-test.c 
b/tests/api/api-matroska-t35-test.c
new file mode 100644
index 00000000000..ef3144815ed
--- /dev/null
+++ b/tests/api/api-matroska-t35-test.c
@@ -0,0 +1,716 @@
+/*
+ * Copyright (c) 2026 luoye-cpu
+ *
+ * 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
+ */
+
+/*
+ * Muxes packets that carry more than one kind of ITU-T T.35 metadata and
+ * checks the TrackEntry the Matroska muxer writes for them: every kind has to
+ * end up under its own BlockAddID, every such ID has to be described by a
+ * BlockAdditionMapping, and the BlockAddIDExtraData of that mapping has to be
+ * the T.35 header of the payload written under it.  The BlockAddIDs also have
+ * to be unique within one BlockAdditions, which is where the muxer used to
+ * give all of them the same value.
+ *
+ * Output that cannot be seeked back to has nowhere to put the mappings, so a
+ * track that ends up carrying more than one kind cannot describe them there.
+ * The muxer keeps writing the value 4 for every payload in that case, which is
+ * what it did before this change: such a file repeats one BlockAddID and does
+ * not meet the uniqueness rule above.  That is a limitation of the muxer and
+ * not the intended output, so the streamed case only pins down that the
+ * behaviour did not change.
+ *
+ * The EBML is walked here instead of being read back through the demuxer on
+ * purpose: matroskadec sniffs the T.35 payload, so it resolves both kinds even
+ * when they share one BlockAddID and cannot notice the inconsistency at all.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "libavformat/avformat.h"
+#include "libavutil/hdr_dynamic_metadata.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/mem.h"
+
+enum {
+    KIND_HDR10,
+    KIND_APP5,
+    KIND_LCEVC,
+    NB_KINDS
+};
+
+/* The T.35 headers the muxer puts in front of each kind of payload: the ones
+ * expected as BlockAddIDExtraData once a track has more than one of them. */
+static const struct {
+    int     size;
+    uint8_t data[8];
+} t35_header[NB_KINDS] = {
+    { 5, { 0xB5, 0x00, 0x3C, 0x00, 0x01 } },
+    { 5, { 0xB5, 0x00, 0x90, 0x00, 0x01 } },
+    { 4, { 0xB4, 0x00, 0x50, 0x00 } },
+};
+
+#define MAX_MAPS   8
+#define MAX_BLOCKS 8
+#define MAX_IDS    8
+
+typedef struct Mapping {
+    uint64_t value;
+    uint64_t type;
+    int      extra_size;
+    uint8_t  extra[8];
+} Mapping;
+
+typedef struct Block {
+    int      nb_ids;
+    uint64_t ids[MAX_IDS];
+} Block;
+
+typedef struct Track {
+    int      nb_entries;
+    int      have_max;
+    uint64_t max_add;
+    int      nb_maps;
+    Mapping  maps[MAX_MAPS];
+    int      nb_blocks;
+    Block    blocks[MAX_BLOCKS];
+} Track;
+
+static int failures;
+
+#define CHECK(cond, ...) do {                       \
+    if (!(cond)) {                                  \
+        av_log(NULL, AV_LOG_ERROR, __VA_ARGS__);    \
+        failures++;                                 \
+    }                                               \
+} while (0)
+
+static void fill_hdr10plus(AVDynamicHDRPlus *hp)
+{
+    AVHDRPlusColorTransformParams *pp = &hp->params[0];
+
+    hp->num_windows = 1;
+    hp->targeted_system_display_maximum_luminance = (AVRational){ 1000, 1 };
+    pp->window_upper_left_corner_x  = (AVRational){ 0, 1 };
+    pp->window_upper_left_corner_y  = (AVRational){ 0, 1 };
+    pp->window_lower_right_corner_x = (AVRational){ 1919, 1 };
+    pp->window_lower_right_corner_y = (AVRational){ 1079, 1 };
+    pp->maxscl[0] = (AVRational){ 1, 1 };
+    pp->maxscl[1] = (AVRational){ 1, 1 };
+    pp->maxscl[2] = (AVRational){ 1, 1 };
+    pp->average_maxrgb         = (AVRational){ 1, 1 };
+    pp->fraction_bright_pixels = (AVRational){ 0, 1 };
+    pp->knee_point_x           = (AVRational){ 1, 1 };
+    pp->knee_point_y           = (AVRational){ 1, 1 };
+    pp->color_saturation_weight = (AVRational){ 0, 1 };
+    for (int i = 0; i < 15; i++) {
+        pp->bezier_curve_anchors[i]           = (AVRational){ 1, 1 };
+        pp->distribution_maxrgb[i].percentile = (AVRational){ 1, 1 };
+    }
+}
+
+static int add_hdr10plus(AVPacket *pkt)
+{
+    size_t size = 0;
+    AVDynamicHDRPlus *hp = av_dynamic_hdr_plus_alloc(&size);
+    uint8_t *sd;
+
+    if (!hp)
+        return AVERROR(ENOMEM);
+    fill_hdr10plus(hp);
+    sd = av_packet_new_side_data(pkt, AV_PKT_DATA_DYNAMIC_HDR10_PLUS, size);
+    if (sd)
+        memcpy(sd, hp, size);
+    av_free(hp);
+    return sd ? 0 : AVERROR(ENOMEM);
+}
+
+static int add_app5(AVPacket *pkt)
+{
+    size_t size = 0;
+    AVDynamicHDRSmpte2094App5 *ap = av_dynamic_hdr_smpte2094_app5_alloc(&size);
+    uint8_t *sd;
+
+    if (!ap)
+        return AVERROR(ENOMEM);
+    sd = av_packet_new_side_data(pkt, AV_PKT_DATA_DYNAMIC_HDR_SMPTE_2094_APP5,
+                                 size);
+    if (sd)
+        memcpy(sd, ap, size);
+    av_free(ap);
+    return sd ? 0 : AVERROR(ENOMEM);
+}
+
+static int add_lcevc(AVPacket *pkt)
+{
+    static const uint8_t payload[] = { 0x00, 0x01, 0x02, 0x03,
+                                       0x04, 0x05, 0x06, 0x07 };
+    uint8_t *sd = av_packet_new_side_data(pkt, AV_PKT_DATA_LCEVC,
+                                         sizeof(payload));
+
+    if (sd)
+        memcpy(sd, payload, sizeof(payload));
+    return sd ? 0 : AVERROR(ENOMEM);
+}
+
+/* A minimal avcC, so that the muxer writes a CodecPrivate for the stream. */
+static int add_extradata(AVStream *st)
+{
+    static const uint8_t avcc[] = {
+        0x01, 0x42, 0xc0, 0x0a, 0xff,
+        0xe1, 0x00, 0x09, 0x67, 0x42, 0xc0, 0x0a, 0xd9, 0x1e, 0x84, 0x3b, 0x4b,
+        0x01, 0x00, 0x04, 0x68, 0xce, 0x3c, 0x80,
+    };
+
+    st->codecpar->extradata = av_memdup(avcc, sizeof(avcc));
+    if (!st->codecpar->extradata)
+        return AVERROR(ENOMEM);
+    st->codecpar->extradata_size = sizeof(avcc);
+    return 0;
+}
+
+/* Sink for the run that must not be able to seek. */
+typedef struct MemSink {
+    uint8_t *data;
+    size_t   size;
+    size_t   alloc;
+} MemSink;
+
+static int sink_write(void *opaque, const uint8_t *buf, int size)
+{
+    MemSink *sink = opaque;
+
+    if (sink->size + size > sink->alloc) {
+        size_t alloc = FFMAX(sink->size + size, 2 * sink->alloc + 1024);
+        uint8_t *data = av_realloc(sink->data, alloc);
+
+        if (!data)
+            return AVERROR(ENOMEM);
+        sink->data  = data;
+        sink->alloc = alloc;
+    }
+    memcpy(sink->data + sink->size, buf, size);
+    sink->size += size;
+    return size;
+}
+
+/**
+ * Mux @nb_packets packets, the n-th of them carrying the kinds in
+ * @kinds[n], either into a seekable AVIOContext (avio_open_dyn_buf()) or into
+ * one without a seek function, the way a pipe is handed to the muxer.
+ */
+static int mux_sequence(const unsigned *kinds, int nb_packets, int seekable,
+                        uint8_t **out, int *out_size)
+{
+    static const uint8_t nal[] = { 0x00, 0x00, 0x00, 0x05,
+                                   0x65, 0x88, 0x84, 0x00, 0x21 };
+    AVFormatContext *oc = NULL;
+    AVIOContext *pb = NULL;
+    MemSink sink = { 0 };
+    AVPacket *pkt = NULL;
+    AVStream *st;
+    int ret;
+
+    ret = avformat_alloc_output_context2(&oc, NULL, "matroska", "t35.mkv");
+    if (ret < 0)
+        goto end;
+
+    st = avformat_new_stream(oc, NULL);
+    if (!st) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id   = AV_CODEC_ID_H264;
+    st->codecpar->width      = 64;
+    st->codecpar->height     = 64;
+    st->codecpar->format     = AV_PIX_FMT_YUV420P;
+    st->time_base            = (AVRational){ 1, 25 };
+    st->avg_frame_rate       = (AVRational){ 25, 1 };
+    ret = add_extradata(st);
+    if (ret < 0)
+        goto end;
+
+    if (seekable) {
+        ret = avio_open_dyn_buf(&pb);
+        if (ret < 0)
+            goto end;
+    } else {
+        uint8_t *buf = av_malloc(32768);
+
+        if (!buf) {
+            ret = AVERROR(ENOMEM);
+            goto end;
+        }
+        pb = avio_alloc_context(buf, 32768, 1, &sink, NULL, sink_write, NULL);
+        if (!pb) {
+            av_free(buf);
+            ret = AVERROR(ENOMEM);
+            goto end;
+        }
+    }
+    oc->pb = pb;
+
+    ret = avformat_write_header(oc, NULL);
+    if (ret < 0)
+        goto end;
+
+    pkt = av_packet_alloc();
+    if (!pkt) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+
+    for (int i = 0; i < nb_packets; i++) {
+        unsigned mask = kinds[i];
+
+        ret = av_new_packet(pkt, sizeof(nal));
+        if (ret < 0)
+            goto end;
+        memcpy(pkt->data, nal, sizeof(nal));
+        pkt->pts = pkt->dts = i;
+        pkt->duration       = 1;
+        pkt->flags         |= AV_PKT_FLAG_KEY;
+
+        if ((mask & (1 << KIND_HDR10) && (ret = add_hdr10plus(pkt)) < 0) ||
+            (mask & (1 << KIND_APP5)  && (ret = add_app5(pkt))  < 0)     ||
+            (mask & (1 << KIND_LCEVC) && (ret = add_lcevc(pkt)) < 0))
+            goto end;
+
+        ret = av_interleaved_write_frame(oc, pkt);
+        if (ret < 0)
+            goto end;
+        av_packet_unref(pkt);
+    }
+
+    ret = av_write_trailer(oc);
+    if (ret < 0)
+        goto end;
+    /* av_write_trailer() flushes, but the write callback can still have
+     * failed; without this the test would pass on a run that dropped output. 
*/
+    if (pb->error < 0) {
+        ret = pb->error;
+        goto end;
+    }
+
+    if (seekable) {
+        *out_size = avio_close_dyn_buf(pb, out);
+        oc->pb    = NULL;
+        pb        = NULL;
+    } else {
+        avio_flush(pb);
+        if (pb->error < 0) {
+            ret = pb->error;
+            goto end;
+        }
+        *out      = sink.data;
+        *out_size = (int)sink.size;
+        sink.data = NULL;
+        /* avio_context_free() does not free the buffer it was given. */
+        av_freep(&pb->buffer);
+        avio_context_free(&pb);
+        oc->pb = NULL;
+    }
+
+end:
+    if (ret < 0)
+        av_freep(&sink.data);
+    av_packet_free(&pkt);
+    if (oc)
+        oc->pb = NULL;
+    if (pb) {
+        if (seekable) {
+            uint8_t *discard = NULL;
+
+            if (avio_close_dyn_buf(pb, &discard) < 0)
+                ret = ret < 0 ? ret : AVERROR(EIO);
+            av_free(discard);
+        } else {
+            av_freep(&pb->buffer);
+            avio_context_free(&pb);
+        }
+    }
+    avformat_free_context(oc);
+    return ret;
+}
+
+/* ------------------------------------------------------------- EBML walk */
+
+#define ID_SEGMENT      0x18538067
+#define ID_TRACKS       0x1654AE6B
+#define ID_TRACKENTRY   0xAE
+#define ID_MAXBLKADDID  0x55EE
+#define ID_MAPPING      0x41E4
+#define ID_MAP_VALUE    0x41F0
+#define ID_MAP_TYPE     0x41E7
+#define ID_MAP_EXTRA    0x41ED
+#define ID_CLUSTER      0x1F43B675
+#define ID_BLOCKGROUP   0xA0
+#define ID_BLOCKADDS    0x75A1
+#define ID_BLOCKMORE    0xA6
+#define ID_BLOCKADDID   0xEE
+
+#define T35_TYPE        4
+
+static const uint8_t *read_id(const uint8_t *p, const uint8_t *end,
+                              uint32_t *id)
+{
+    int len;
+    uint32_t v = 0;
+
+    if (p >= end)
+        return NULL;
+    if      (p[0] & 0x80) len = 1;
+    else if (p[0] & 0x40) len = 2;
+    else if (p[0] & 0x20) len = 3;
+    else if (p[0] & 0x10) len = 4;
+    else                  return NULL;
+    if (end - p < len)
+        return NULL;
+    for (int i = 0; i < len; i++)
+        v = (v << 8) | p[i];
+    *id = v;
+    return p + len;
+}
+
+static const uint8_t *read_size(const uint8_t *p, const uint8_t *end,
+                               uint64_t *size, int *unknown)
+{
+    int len = 1, i;
+    uint8_t mask = 0x80;
+    uint64_t v;
+
+    if (p >= end)
+        return NULL;
+    while (len <= 8 && !(p[0] & mask)) {
+        mask >>= 1;
+        len++;
+    }
+    if (len > 8 || end - p < len)
+        return NULL;
+    v = p[0] & (mask - 1);
+    for (i = 1; i < len; i++)
+        v = (v << 8) | p[i];
+    *unknown = v == (((uint64_t)1 << (7 * len)) - 1);
+    *size    = v;
+    return p + len;
+}
+
+/* Every integer element read here holds a small number, so anything wider than
+ * eight bytes is a sign that the walk has lost its place. */
+static int read_uint(const uint8_t *p, const uint8_t *end, uint64_t *v)
+{
+    uint64_t val = 0;
+
+    if (end - p > 8)
+        return AVERROR_INVALIDDATA;
+    while (p < end)
+        val = (val << 8) | *p++;
+    *v = val;
+    return 0;
+}
+
+/* Collect what the checks need; map_idx is the mapping being read, or -1. */
+static int walk(const uint8_t *p, const uint8_t *end, Track *t, int map_idx)
+{
+    while (p < end) {
+        const uint8_t *body, *body_end;
+        uint32_t id;
+        uint64_t size;
+        int unknown, ret;
+
+        p = read_id(p, end, &id);
+        if (!p)
+            return AVERROR_INVALIDDATA;
+        p = read_size(p, end, &size, &unknown);
+        if (!p)
+            return AVERROR_INVALIDDATA;
+        /* An unknown size reaches to the end of the parent, which is what a
+         * streamed Segment and the Cluster being built get. */
+        if (!unknown && size > (uint64_t)(end - p))
+            return AVERROR_INVALIDDATA;
+        body     = p;
+        body_end = unknown ? end : p + size;
+        p        = body_end;
+
+        switch (id) {
+        case ID_SEGMENT:
+        case ID_TRACKS:
+        case ID_CLUSTER:
+        case ID_BLOCKGROUP:
+        case ID_BLOCKMORE:
+            ret = walk(body, body_end, t, map_idx);
+            if (ret < 0)
+                return ret;
+            break;
+        case ID_TRACKENTRY:
+            t->nb_entries++;
+            ret = walk(body, body_end, t, map_idx);
+            if (ret < 0)
+                return ret;
+            break;
+        case ID_MAXBLKADDID:
+            if (read_uint(body, body_end, &t->max_add) < 0)
+                return AVERROR_INVALIDDATA;
+            t->have_max = 1;
+            break;
+        case ID_MAPPING:
+            if (t->nb_maps >= MAX_MAPS)
+                return AVERROR_INVALIDDATA;
+            memset(&t->maps[t->nb_maps], 0, sizeof(t->maps[0]));
+            ret = walk(body, body_end, t, t->nb_maps);
+            if (ret < 0)
+                return ret;
+            t->nb_maps++;
+            break;
+        case ID_MAP_VALUE:
+            if (map_idx < 0 ||
+                read_uint(body, body_end, &t->maps[map_idx].value) < 0)
+                return AVERROR_INVALIDDATA;
+            break;
+        case ID_MAP_TYPE:
+            if (map_idx < 0 ||
+                read_uint(body, body_end, &t->maps[map_idx].type) < 0)
+                return AVERROR_INVALIDDATA;
+            break;
+        case ID_MAP_EXTRA:
+            if (map_idx < 0 ||
+                body_end - body > (ptrdiff_t)sizeof(t->maps[0].extra))
+                return AVERROR_INVALIDDATA;
+            t->maps[map_idx].extra_size = body_end - body;
+            memcpy(t->maps[map_idx].extra, body, body_end - body);
+            break;
+        case ID_BLOCKADDS:
+            if (t->nb_blocks >= MAX_BLOCKS)
+                return AVERROR_INVALIDDATA;
+            t->blocks[t->nb_blocks].nb_ids = 0;
+            t->nb_blocks++;
+            ret = walk(body, body_end, t, map_idx);
+            if (ret < 0)
+                return ret;
+            break;
+        case ID_BLOCKADDID: {
+            Block *b;
+            uint64_t v;
+
+            if (read_uint(body, body_end, &v) < 0)
+                return AVERROR_INVALIDDATA;
+            if (!t->nb_blocks || t->blocks[t->nb_blocks - 1].nb_ids >= MAX_IDS)
+                return AVERROR_INVALIDDATA;
+            b = &t->blocks[t->nb_blocks - 1];
+            b->ids[b->nb_ids++] = v;
+            break;
+        }
+        default:
+            break;
+        }
+    }
+    return 0;
+}
+
+/* One muxing run: what to call it, whether the output can be seeked back to,
+ * and the kinds each packet carries, in the order the packets are written. */
+#define MAX_PACKETS 4
+
+typedef struct Spec {
+    const char *name;
+    int         seekable;
+    int         nb_packets;
+    unsigned    kinds[MAX_PACKETS];
+} Spec;
+
+/**
+ * Check the TrackEntry and the Blocks of one muxed file.
+ *
+ * The muxer hands one value to each kind of T.35 payload, in the order the
+ * kinds are first seen on the track: 4 for the first one, 5 for the next and
+ * so on, and a kind that recurs keeps the value it was given.  With seekable
+ * output each of them is described by a mapping whose extra data is that
+ * kind's T.35 header.  Without, no mapping can be written at all, and there
+ * every payload keeps the value 4 the muxer wrote before this change.
+ */
+static void check(const Track *t, const Spec *spec, int seekable)
+{
+    const char *what = spec->name;
+    uint64_t expected[NB_KINDS] = { 0 };
+    int nb_kinds = 0, nb_blocks = 0, b;
+
+    /* First-seen order: the packets in write order, and inside one packet the
+     * kinds in the order the muxer writes them, which is the order of the
+     * array the headers are in. */
+    for (int n = 0; n < spec->nb_packets; n++) {
+        for (int k = 0; k < NB_KINDS; k++) {
+            if ((spec->kinds[n] & (1 << k)) && !expected[k]) {
+                expected[k] = 4 + (seekable ? nb_kinds : 0);
+                nb_kinds++;
+            }
+        }
+        nb_blocks += !!spec->kinds[n];
+    }
+
+    CHECK(t->nb_entries == 1, "%s: expected one TrackEntry, got %d\n",
+          what, t->nb_entries);
+    CHECK(t->nb_blocks == nb_blocks,
+          "%s: expected %d BlockAdditions, got %d\n",
+          what, nb_blocks, t->nb_blocks);
+
+    if (!seekable) {
+        /* Neither the mappings nor MaxBlockAdditionID can be written back to,
+         * so a track that carries more than one kind repeats BlockAddID 4 and
+         * does not meet the uniqueness rule of RFC 9559 section 5.1.3.5.2.3.
+         * That is what the muxer did before this change and all that this case
+         * pins down; it is not the output the format asks for. */
+        for (b = 0; b < t->nb_blocks; b++)
+            for (int i = 0; i < t->blocks[b].nb_ids; i++)
+                CHECK(t->blocks[b].ids[i] == 4,
+                      "%s: BlockMore %d of BlockAdditions %d has BlockAddID "
+                      "%"PRIu64", expected 4\n",
+                      what, i, b, t->blocks[b].ids[i]);
+        CHECK(!t->have_max, "%s: MaxBlockAdditionID written for streamed "
+              "output\n", what);
+        CHECK(t->nb_maps == 0, "%s: %d BlockAdditionMappings written for "
+              "streamed output\n", what, t->nb_maps);
+        return;
+    }
+
+    if (t->nb_blocks != nb_blocks)
+        return;
+
+    /* Every payload of one packet has its own ID, and a kind keeps the one it
+     * was given when it turns up again in a later packet: that is the
+     * uniqueness rule of RFC 9559 section 5.1.3.5.2.3, which the muxer used to
+     * break by writing 4 for all of them. */
+    for (b = 0; b < t->nb_blocks; b++) {
+        int nb = 0;
+
+        for (int k = 0; k < NB_KINDS; k++) {
+            uint64_t got = 0;
+
+            if (!(spec->kinds[b] & (1 << k)))
+                continue;
+            if (nb < t->blocks[b].nb_ids)
+                got = t->blocks[b].ids[nb];
+            CHECK(nb < t->blocks[b].nb_ids && got == expected[k],
+                  "%s: BlockAdditions %d, BlockMore %d has BlockAddID "
+                  "%"PRIu64", expected %"PRIu64"\n",
+                  what, b, nb, got, expected[k]);
+            nb++;
+        }
+        CHECK(t->blocks[b].nb_ids == nb,
+              "%s: expected %d BlockMores in BlockAdditions %d, got %d\n",
+              what, nb, b, t->blocks[b].nb_ids);
+    }
+
+    CHECK(t->nb_maps == nb_kinds, "%s: expected %d BlockAdditionMappings, "
+          "got %d\n", what, nb_kinds, t->nb_maps);
+    CHECK(t->have_max, "%s: no MaxBlockAdditionID\n", what);
+    CHECK(t->max_add == (uint64_t)(3 + nb_kinds),
+          "%s: MaxBlockAdditionID is %"PRIu64", expected %d\n",
+          what, t->max_add, 3 + nb_kinds);
+
+    for (int k = 0; k < NB_KINDS; k++) {
+        const Mapping *m = NULL;
+
+        if (!expected[k])
+            continue;
+        for (int i = 0; i < t->nb_maps; i++)
+            if (t->maps[i].value == expected[k])
+                m = &t->maps[i];
+        CHECK(m, "%s: no mapping for BlockAddID %"PRIu64"\n",
+              what, expected[k]);
+        if (!m)
+            continue;
+        CHECK(m->type == T35_TYPE, "%s: mapping %"PRIu64" has type %"PRIu64
+              ", expected %d\n", what, m->value, m->type, T35_TYPE);
+        if (nb_kinds == 1) {
+            /* A single T.35 mapping needs no extra data, and the muxer leaves
+             * it out so that such a track keeps the bytes it had before. */
+            CHECK(m->extra_size == 0, "%s: extra data on the only mapping\n",
+                  what);
+        } else {
+            CHECK(m->extra_size == t35_header[k].size &&
+                  !memcmp(m->extra, t35_header[k].data, t35_header[k].size),
+                  "%s: extra data of mapping %"PRIu64" is not the T.35 "
+                  "header of the payload under it\n", what, m->value);
+        }
+    }
+}
+
+static void run(const Spec *spec)
+{
+    const char *what = spec->name;
+    uint8_t *buf = NULL;
+    Track t = { 0 };
+    int size = 0, ret;
+
+    av_log(NULL, AV_LOG_INFO, "%s\n", what);
+    ret = mux_sequence(spec->kinds, spec->nb_packets, spec->seekable,
+                       &buf, &size);
+    if (ret < 0) {
+        av_log(NULL, AV_LOG_ERROR, "%s: muxing failed: %s\n",
+               what, av_err2str(ret));
+        failures++;
+        return;
+    }
+    ret = walk(buf, buf + size, &t, -1);
+    if (ret < 0) {
+        av_log(NULL, AV_LOG_ERROR, "%s: could not parse the output\n", what);
+        failures++;
+    } else {
+        check(&t, spec, spec->seekable);
+    }
+    av_free(buf);
+}
+
+int main(void)
+{
+    static const Spec specs[] = {
+        { "seekable, one kind, HDR10+", 1, 1, { 1 << KIND_HDR10 } },
+        { "seekable, one kind, App5",   1, 1, { 1 << KIND_APP5 } },
+        { "seekable, one kind, LCEVC",  1, 1, { 1 << KIND_LCEVC } },
+        { "seekable, three kinds on one packet", 1, 1, {
+             (1 << NB_KINDS) - 1,
+        } },
+        /* The order the kinds first appear in is what hands out the values, so
+         * it has to be seen across packets and not only inside one. */
+        { "seekable, first seen across packets", 1, 4, {
+             1 << KIND_APP5,
+             1 << KIND_HDR10,
+             1 << KIND_LCEVC,
+             (1 << KIND_APP5) | (1 << KIND_HDR10),
+        } },
+        { "streamed, three kinds on one packet", 0, 1, {
+             (1 << NB_KINDS) - 1,
+        } },
+        { "streamed, first seen across packets", 0, 2, {
+             1 << KIND_HDR10,
+             (1 << KIND_APP5) | (1 << KIND_LCEVC),
+        } },
+    };
+
+    av_log_set_level(AV_LOG_INFO);
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(specs); i++)
+        run(&specs[i]);
+
+    av_log(NULL, AV_LOG_INFO, failures ? "test failed\n" : "test passed\n");
+    return !!failures;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index 1659c7979c0..1fad1de8cfd 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -50,6 +50,11 @@ fate-lavf-flv: CMP =
 fate-api-seek: CMD = run $(APITESTSDIR)/api-seek-test$(EXESUF) 
$(TARGET_PATH)/tests/data/lavf/lavf.flv 0 720
 fate-api-seek: CMP = null
 
+FATE_API_LIBAVFORMAT-$(CONFIG_MATROSKA_MUXER) += fate-api-matroska-t35
+fate-api-matroska-t35: $(APITESTSDIR)/api-matroska-t35-test$(EXESUF)
+fate-api-matroska-t35: CMD = run $(APITESTSDIR)/api-matroska-t35-test$(EXESUF)
+fate-api-matroska-t35: CMP = null
+
 FATE_API_LIBAVFORMAT-$(call ALLYES, MP4_MUXER ISMV_MUXER) += fate-api-movenc
 fate-api-movenc: $(APITESTSDIR)/api-movenc-test$(EXESUF)
 fate-api-movenc: CMD = run $(APITESTSDIR)/api-movenc-test$(EXESUF)
-- 
2.54.0

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

Reply via email to