PR #23748 opened by Jun Zhao (mypopydev) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23748 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23748.patch
These two commits improve DTS profile detection for MP4 files carrying a ddts box. The demuxer change (mov) parses the DTS profile and channel layout directly from the ddts box, and the decoder change (dca) makes the codec trust the bitstream-derived profile once parsing succeeds, ensuring the correct profile and channel configuration are used during decoding. >From 0b8766484207686637625207fb1dcca66a7376a2 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 9 Jul 2026 07:39:49 +0800 Subject: [PATCH 1/2] avformat/mov: read DTS profile and channel layout from ddts box Parse the full DTSSpecificBox per ETSI TS 102 114: map StreamConstruction (Table E-2) to codecpar->profile and ChannelLayout (Table E-5) to ch_layout. This gives ffprobe correct metadata for DTS-in-MP4, including CENC-encrypted tracks where sample data is unreadable. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/mov.c | 95 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/libavformat/mov.c b/libavformat/mov.c index 4154038d9e..fccbbb3287 100644 --- a/libavformat/mov.c +++ b/libavformat/mov.c @@ -1166,6 +1166,75 @@ static int mov_read_dec3(MOVContext *c, AVIOContext *pb, MOVAtom atom) return 0; } +/* Maps the DTS `StreamConstruction` field (ETSI TS 102 114 Table E-2) to the + * coarse ffmpeg DTS profile enum. The mapping mirrors the precedence used by + * the DTS parser (libavcodec/dca_parser.c): XLL => lossless (HD_MA), otherwise + * XXCH/XBR => HD_HRA, X96 => 96/24, XCH => ES, core-only => DTS, LBR => Express. + * Index 0 (undefined) and any value outside 1..21 stay AV_PROFILE_UNKNOWN so the + * parser can derive the profile from the bitstream instead. + * This is the same "container declares a profile-only field; set codecpar->profile + * directly" pattern used by the ARIB caption demuxers (matroskadec.c, mpegts.c). */ +static int mov_dts_stream_construction_to_profile(unsigned int sc) +{ + static const int profile[22] = { + [ 1] = AV_PROFILE_DTS, /* core only */ + [ 2] = AV_PROFILE_DTS_ES, /* core + XCH (DTS-ES) */ + [ 3] = AV_PROFILE_DTS_96_24, /* core + X96 */ + [ 4] = AV_PROFILE_DTS_96_24, /* core + XCH + X96 */ + [ 5] = AV_PROFILE_DTS_HD_HRA, /* core + XXCH */ + [ 6] = AV_PROFILE_DTS_HD_HRA, /* core + XXCH + X96 */ + [ 7] = AV_PROFILE_DTS_HD_HRA, /* core + XCH + XXCH */ + [ 8] = AV_PROFILE_DTS_HD_HRA, /* core + X96 + XXCH */ + [ 9] = AV_PROFILE_DTS_HD_HRA, /* core + XXCH + X96 */ + [10] = AV_PROFILE_DTS_HD_HRA, /* core + XXCH + X96 */ + [11] = AV_PROFILE_DTS_HD_HRA, /* core + XCH + XXCH + X96 */ + [12] = AV_PROFILE_DTS_HD_HRA, /* core + X96 + XXCH */ + [13] = AV_PROFILE_DTS_HD_MA, /* core + XXCH + X96 + XLL */ + [14] = AV_PROFILE_DTS_HD_MA, /* core + XXCH + XLL */ + [15] = AV_PROFILE_DTS_HD_MA, /* core + XCH + XLL */ + [16] = AV_PROFILE_DTS_HD_MA, /* core + X96 + XLL */ + [17] = AV_PROFILE_DTS_HD_MA, /* XXCH + XLL, no core (dtsl) */ + [18] = AV_PROFILE_DTS_EXPRESS, /* LBR (DTS Express) */ + [19] = AV_PROFILE_DTS_HD_HRA, /* XXCH, core in extension substream */ + [20] = AV_PROFILE_DTS_HD_MA, /* XXCH + XLL, core in ext substream */ + [21] = AV_PROFILE_DTS_HD_MA, /* XLL, core in ext substream (DTS:X)*/ + }; + if (!sc || sc > 21) + return AV_PROFILE_UNKNOWN; + return profile[sc]; +} + +/* Maps the 16-bit DTS `ChannelLayout` bitmask (ETSI TS 102 114 Table E-5) to an + * ffmpeg channel mask. Pair bits follow Table 7-10 / DCA_SPEAKER_PAIR_*; + * LwRw (0x0400) and LssRss (0x0800) must not collapse onto SIDE like LsRs. */ +static uint64_t mov_dts_channel_layout_to_mask(unsigned int code) +{ + static const struct { unsigned int dts; uint64_t av; } map[] = { + { 0x0001, AV_CH_FRONT_CENTER }, + { 0x0002, AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT }, + { 0x0004, AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT }, + { 0x0008, AV_CH_LOW_FREQUENCY }, + { 0x0010, AV_CH_BACK_CENTER }, + { 0x0020, AV_CH_TOP_FRONT_LEFT | AV_CH_TOP_FRONT_RIGHT }, + { 0x0040, AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT }, + { 0x0080, AV_CH_TOP_FRONT_CENTER }, + { 0x0100, AV_CH_TOP_CENTER }, + { 0x0200, AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER }, + { 0x0400, AV_CH_WIDE_LEFT | AV_CH_WIDE_RIGHT }, + { 0x0800, AV_CH_SIDE_SURROUND_LEFT | AV_CH_SIDE_SURROUND_RIGHT }, + { 0x1000, AV_CH_LOW_FREQUENCY_2 }, + { 0x2000, AV_CH_TOP_SIDE_LEFT | AV_CH_TOP_SIDE_RIGHT }, + { 0x4000, AV_CH_TOP_BACK_CENTER }, + { 0x8000, AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT }, + }; + uint64_t mask = 0; + int i; + for (i = 0; i < FF_ARRAY_ELEMS(map); i++) + if (code & map[i].dts) + mask |= map[i].av; + return mask; +} + static int mov_read_ddts(MOVContext *c, AVIOContext *pb, MOVAtom atom) { #define DDTS_SIZE 20 @@ -1173,6 +1242,7 @@ static int mov_read_ddts(MOVContext *c, AVIOContext *pb, MOVAtom atom) AVStream *st = NULL; uint32_t frame_duration_code = 0; uint32_t channel_layout_code = 0; + unsigned int stream_construction; GetBitContext gb; int ret; @@ -1195,7 +1265,12 @@ static int mov_read_ddts(MOVContext *c, AVIOContext *pb, MOVAtom atom) st->codecpar->bit_rate = get_bits_long(&gb, 32); st->codecpar->bits_per_coded_sample = get_bits(&gb, 8); frame_duration_code = get_bits(&gb, 2); - skip_bits(&gb, 30); /* various fields */ + stream_construction = get_bits(&gb, 5); /* Table E-2 - profile source */ + skip_bits(&gb, 1); /* CoreLFEPresent */ + skip_bits(&gb, 6); /* CoreLayout */ + skip_bits(&gb, 14); /* CoreSize */ + skip_bits(&gb, 1); /* StereoDownmix */ + skip_bits(&gb, 3); /* RepresentationType */ channel_layout_code = get_bits(&gb, 16); st->codecpar->frame_size = @@ -1204,17 +1279,17 @@ static int mov_read_ddts(MOVContext *c, AVIOContext *pb, MOVAtom atom) (frame_duration_code == 2) ? 2048 : (frame_duration_code == 3) ? 4096 : 0; - if (channel_layout_code > 0xff) { - av_log(c->fc, AV_LOG_WARNING, "Unsupported DTS audio channel layout\n"); - } + /* Always publish StreamConstruction as codecpar->profile for ffprobe and + * for encrypted tracks where the parser cannot read frame headers. When + * samples are plaintext the DTS parser overwrites avctx->profile from the + * bitstream; on parse failure it keeps this demuxer-provided value. */ + st->codecpar->profile = mov_dts_stream_construction_to_profile(stream_construction); + av_channel_layout_uninit(&st->codecpar->ch_layout); av_channel_layout_from_mask(&st->codecpar->ch_layout, - ((channel_layout_code & 0x1) ? AV_CH_FRONT_CENTER : 0) | - ((channel_layout_code & 0x2) ? AV_CH_FRONT_LEFT : 0) | - ((channel_layout_code & 0x2) ? AV_CH_FRONT_RIGHT : 0) | - ((channel_layout_code & 0x4) ? AV_CH_SIDE_LEFT : 0) | - ((channel_layout_code & 0x4) ? AV_CH_SIDE_RIGHT : 0) | - ((channel_layout_code & 0x8) ? AV_CH_LOW_FREQUENCY : 0)); + mov_dts_channel_layout_to_mask(channel_layout_code)); + if (!st->codecpar->ch_layout.nb_channels) + av_channel_layout_default(&st->codecpar->ch_layout, 2); return 0; } -- 2.52.0 >From 7a455a87efefcfcb2bc15f0fe445095ba0d4f859 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 9 Jul 2026 07:39:49 +0800 Subject: [PATCH 2/2] avcodec/dca: prefer bitstream-derived DTS profile when parsing succeeds Remove the parser trust-guard that skipped profile derivation when the demuxer had already set avctx->profile. The parser now overwrites profile from frame headers on success and keeps the demuxer-provided value when parse fails (e.g. encrypted samples). Warn once when container and bitstream profiles disagree. Signed-off-by: Jun Zhao <[email protected]> --- libavcodec/dca_parser.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/libavcodec/dca_parser.c b/libavcodec/dca_parser.c index b7e2add698..c6cbb76f18 100644 --- a/libavcodec/dca_parser.c +++ b/libavcodec/dca_parser.c @@ -39,6 +39,7 @@ typedef struct DCAParseContext { unsigned int startpos; DCAExssParser exss; unsigned int sr_code; + int profile_mismatch_warned; } DCAParseContext; #define IS_CORE_MARKER(state) \ @@ -270,9 +271,10 @@ static int dca_parse_params(DCAParseContext *pc1, const uint8_t *buf, *duration = h.npcmblocks * DCA_PCMBLOCK_SAMPLES; *sample_rate = ff_dca_sample_rates[h.sr_code]; - if (*profile != AV_PROFILE_UNKNOWN) - return 0; + /* Prefer a bitstream-derived profile when the frame header parses. + * On failure (e.g. encrypted samples) the caller keeps *profile as set + * by the demuxer via codecpar (see mov_read_ddts). */ *profile = AV_PROFILE_DTS; if (h.ext_audio_present) { switch (h.ext_audio_type) { @@ -305,6 +307,26 @@ static int dca_parse_params(DCAParseContext *pc1, const uint8_t *buf, return 0; } +static void dca_warn_profile_mismatch(AVCodecContext *avctx, int container_profile, + int bitstream_profile, int *warned) +{ + const char *container_name, *bitstream_name; + + if (*warned || container_profile == AV_PROFILE_UNKNOWN || + bitstream_profile == AV_PROFILE_UNKNOWN || + container_profile == bitstream_profile) + return; + + container_name = avcodec_profile_name(AV_CODEC_ID_DTS, container_profile); + bitstream_name = avcodec_profile_name(AV_CODEC_ID_DTS, bitstream_profile); + av_log(avctx, AV_LOG_WARNING, + "DTS profile %s from container disagrees with bitstream profile %s; " + "using bitstream value\n", + container_name ? container_name : "unknown", + bitstream_name ? bitstream_name : "unknown"); + *warned = 1; +} + static int dca_parse(AVCodecParserContext *s, AVCodecContext *avctx, const uint8_t **poutbuf, int *poutbuf_size, const uint8_t *buf, int buf_size) @@ -312,6 +334,7 @@ static int dca_parse(AVCodecParserContext *s, AVCodecContext *avctx, DCAParseContext *pc1 = s->priv_data; ParseContext *pc = &pc1->pc; int next, duration, sample_rate; + int container_profile; if (s->flags & PARSER_FLAG_COMPLETE_FRAMES) { next = buf_size; @@ -333,7 +356,10 @@ static int dca_parse(AVCodecParserContext *s, AVCodecContext *avctx, } /* read the duration and sample rate from the frame header */ + container_profile = avctx->profile; if (!dca_parse_params(pc1, buf, buf_size, &duration, &sample_rate, &avctx->profile)) { + dca_warn_profile_mismatch(avctx, container_profile, avctx->profile, + &pc1->profile_mismatch_warned); if (!avctx->sample_rate) avctx->sample_rate = sample_rate; s->duration = av_rescale(duration, avctx->sample_rate, sample_rate); -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
