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]