Subject: [PATCH] avformat: Demuxing of DTS-UHD input files per ETSI TS 102 114 is added as demuxer "dtsuhd". movenc supports DTS-UHD audio track.
From: Roy Funderburk <[email-masked]>
To: [email-masked]
Date: Tue, 16 Jan 2024 11:45:59 -0800

Signed-off-by: Roy Funderburk <[email-masked]>
---
 Changelog                 |   1 +
 configure                 |   1 +
 doc/general_contents.texi |   1 +
 libavformat/Makefile      |   1 +
 libavformat/allformats.c  |   1 +
 libavformat/dtshddec.c    |   2 +-
 libavformat/dtsuhddec.c   | 216 ++++++++++++++++++++++++++++++++++++++
 libavformat/movenc.c      |  32 ++++++
 libavformat/version.h     |   2 +-
 9 files changed, 255 insertions(+), 2 deletions(-)
 create mode 100644 libavformat/dtsuhddec.c

diff --git a/Changelog b/Changelog
index 4e7c1ce2c1..d1f19d7047 100644
--- a/Changelog
+++ b/Changelog
@@ -19,6 +19,7 @@ version <next>:
 - VVC decoder
 - fsync filter
 - Raw Captions with Time (RCWT) closed caption muxer
+- DTS-UHD demuxer
 
 version 6.1:
 - libaribcaption decoder
diff --git a/configure b/configure
index c8ae0a061d..70f511827e 100755
--- a/configure
+++ b/configure
@@ -3517,6 +3517,7 @@ dash_demuxer_deps="libxml2"
 dirac_demuxer_select="dirac_parser"
 dts_demuxer_select="dca_parser"
 dtshd_demuxer_select="dca_parser"
+dtsuhd_demuxer_select="dtsuhd_parser"
 dv_demuxer_select="dvprofile"
 dv_muxer_select="dvprofile"
 dxa_demuxer_select="riffdec"
diff --git a/doc/general_contents.texi b/doc/general_contents.texi
index 8b48fed060..2c6d008039 100644
--- a/doc/general_contents.texi
+++ b/doc/general_contents.texi
@@ -613,6 +613,7 @@ library:
 @item raw DNxHD                 @tab X @tab X
 @item raw DTS                   @tab X @tab X
 @item raw DTS-HD                @tab   @tab X
+@item raw DTS-UHD               @tab   @tab
 @item raw E-AC-3                @tab X @tab X
 @item raw EVC                   @tab X @tab X
 @item raw FLAC                  @tab X @tab X
diff --git a/libavformat/Makefile b/libavformat/Makefile
index dcc99eeac4..f98f157ab0 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -188,6 +188,7 @@ OBJS-$(CONFIG_DSICIN_DEMUXER)            += dsicin.o
 OBJS-$(CONFIG_DSS_DEMUXER)               += dss.o
 OBJS-$(CONFIG_DTSHD_DEMUXER)             += dtshddec.o
 OBJS-$(CONFIG_DTS_DEMUXER)               += dtsdec.o rawdec.o
+OBJS-$(CONFIG_DTSUHD_DEMUXER)            += dtsuhddec.o
 OBJS-$(CONFIG_DTS_MUXER)                 += rawenc.o
 OBJS-$(CONFIG_DV_MUXER)                  += dvenc.o
 OBJS-$(CONFIG_DVBSUB_DEMUXER)            += dvbsub.o rawdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index b04b43cab3..5e0608de7f 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -146,6 +146,7 @@ extern const AVInputFormat  ff_dss_demuxer;
 extern const AVInputFormat  ff_dts_demuxer;
 extern const FFOutputFormat ff_dts_muxer;
 extern const AVInputFormat  ff_dtshd_demuxer;
+extern const AVInputFormat  ff_dtsuhd_demuxer;
 extern const AVInputFormat  ff_dv_demuxer;
 extern const FFOutputFormat ff_dv_muxer;
 extern const AVInputFormat  ff_dvbsub_demuxer;
diff --git a/libavformat/dtshddec.c b/libavformat/dtshddec.c
index a3dea0668f..6e9e78a335 100644
--- a/libavformat/dtshddec.c
+++ b/libavformat/dtshddec.c
@@ -46,7 +46,7 @@ typedef struct DTSHDDemuxContext {
 static int dtshd_probe(const AVProbeData *p)
 {
     if (AV_RB64(p->buf) == DTSHDHDR)
-        return AVPROBE_SCORE_MAX;
+        return AVPROBE_SCORE_MAX - 4; // DTSUHD (.dtsx) files also have this signature.
     return 0;
 }
 
diff --git a/libavformat/dtsuhddec.c b/libavformat/dtsuhddec.c
new file mode 100644
index 0000000000..d840c0a033
--- /dev/null
+++ b/libavformat/dtsuhddec.c
@@ -0,0 +1,216 @@
+/*
+ * DTS-UHD audio demuxer
+ * Copyright (c) 2023 Xperi Corporation / DTS, Inc.
+ *
+ * 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
+ */
+
+/**
+ * @file
+ * Report DTS-UHD audio stream configuration and extract raw packet data.
+ */
+
+#include "internal.h"
+#include "libavcodec/dtsuhd_common.h"
+#include "libavcodec/put_bits.h"
+#include "libavutil/intreadwrite.h"
+
+#define DTSUHD_BUFFER_SIZE (1024 * 1024)
+
+typedef struct DTSUHDDemuxContext {
+    size_t data_end;
+    struct DTSUHD *dtsuhd;
+    uint8_t *buf;
+} DTSUHDDemuxContext;
+
+static int probe(const AVProbeData *p)
+{
+    int offset = av_dtsuhd_strmdata_payload(p->buf, p->buf_size, NULL);
+    int score = 0;
+    struct DTSUHD *h = av_dtsuhd_create();
+
+    if (h && offset >= 0) {
+        for (; offset + 4 < p->buf_size; offset++) {
+            if (dtsuhd_is_syncword(AV_RB32(p->buf + offset))) {
+                if (DTSUHD_OK == av_dtsuhd_frame(h, p->buf + offset, p->buf_size - offset, NULL, NULL)) {
+                    score = AVPROBE_SCORE_MAX - 3;
+                    break;
+                }
+            }
+        }
+    }
+
+    av_dtsuhd_destroy(h);
+    return score;
+}
+
+static av_cold int read_close(AVFormatContext *s)
+{
+    DTSUHDDemuxContext *dtsxs = s->priv_data;
+
+    av_freep(&dtsxs->buf);
+    av_dtsuhd_destroy(dtsxs->dtsuhd);
+    dtsxs->dtsuhd = NULL;
+
+    return 0;
+}
+
+static int find_first_syncword(DTSUHDDemuxContext *dtsuhd, int data_start)
+{
+    while (data_start + 4 < DTSUHD_BUFFER_SIZE &&
+        !dtsuhd_is_syncword(AV_RB32(dtsuhd->buf + data_start))) {
+        data_start++;
+    }
+
+    return data_start;
+}
+
+static int write_extradata(AVCodecParameters *par, DTSUHDDescriptorInfo *di)
+{
+    PutBitContext pbc;
+    int ret;
+    int size;
+    uint8_t udts[32];
+
+    init_put_bits(&pbc, udts, sizeof(udts));
+    put_bits32(&pbc, 0); // udts box size
+    put_bits32(&pbc, AV_RB32("udts")); // udts box signature
+    put_bits(&pbc, 6, di->decoder_profile_code);
+    put_bits(&pbc, 2, di->frame_duration_code);
+    put_bits(&pbc, 3, di->max_payload_code);
+    put_bits(&pbc, 5, di->num_pres_code);
+    put_bits32(&pbc,  di->channel_mask);
+    put_bits(&pbc, 1, di->base_sample_freq_code);
+    put_bits(&pbc, 2, di->sample_rate_mod);
+    put_bits(&pbc, 3, di->rep_type);
+    put_bits(&pbc, 3, 0);
+    put_bits(&pbc, 1, 0);
+    put_bits64(&pbc, di->num_pres_code + 1, 0); // ID Tag present for each presentation.
+    flush_put_bits(&pbc); // byte align
+    size = put_bytes_output(&pbc);
+    AV_WB32(udts, size);
+
+    ret = ff_alloc_extradata(par, size);
+    if (ret < 0)
+        return ret;
+
+    memcpy(par->extradata, udts, size);
+
+    return 0;
+}
+
+static int read_header(AVFormatContext *s)
+{
+    AVIOContext *pb = s->pb;
+    AVStream *st = avformat_new_stream(s, NULL);
+    DTSUHDDemuxContext *dtsuhd = s->priv_data;
+    DTSUHDDescriptorInfo di;
+    DTSUHDFrameInfo fi;
+    int buf_bytes;
+    int ret = DTSUHD_INVALID_FRAME;
+    int data_start;
+
+    if (!(pb->seekable & AVIO_SEEKABLE_NORMAL))
+        return AVERROR(EIO);
+
+    dtsuhd->buf = av_malloc(DTSUHD_BUFFER_SIZE);
+    dtsuhd->dtsuhd = av_dtsuhd_create();
+    if (!dtsuhd->buf || !dtsuhd->dtsuhd || !st)
+        return AVERROR(ENOMEM);
+
+    buf_bytes = avio_read(pb, dtsuhd->buf, DTSUHD_BUFFER_SIZE);
+    if (buf_bytes < 0)
+        return buf_bytes;
+
+    data_start = av_dtsuhd_strmdata_payload(dtsuhd->buf, buf_bytes, &dtsuhd->data_end);
+    if (data_start < 0)
+        return data_start;
+
+    dtsuhd->data_end += data_start;
+    if (data_start == 0)
+        dtsuhd->data_end = avio_size(pb); // Not a DTSHDHDR chunk file, decode frames to end of file.
+
+    data_start = find_first_syncword(dtsuhd, data_start);
+    if (avio_seek(pb, data_start, SEEK_SET) < 0)
+        return AVERROR(EINVAL);
+
+    ret = av_dtsuhd_frame(dtsuhd->dtsuhd, dtsuhd->buf + data_start,
+        buf_bytes - data_start, &fi, &di);
+    if (ret != DTSUHD_OK || !di.valid) {
+        av_log(s, AV_LOG_ERROR, "Unable to process DTS-UHD file. File may be invalid.\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    ffstream(st)->need_parsing          = AVSTREAM_PARSE_FULL_RAW;
+    st->codecpar->codec_type            = AVMEDIA_TYPE_AUDIO;
+    st->codecpar->codec_id              = s->iformat->raw_codec_id;
+    st->codecpar->ch_layout.order       = AV_CHANNEL_ORDER_NATIVE;
+    st->codecpar->ch_layout.nb_channels = di.channel_count;
+    st->codecpar->ch_layout.u.mask      = di.ffmpeg_channel_mask;
+    st->codecpar->codec_tag             = AV_RL32(di.coding_name);
+    st->codecpar->frame_size            = 512 << di.frame_duration_code;
+    st->codecpar->sample_rate           = di.sample_rate;
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+FF_DISABLE_DEPRECATION_WARNINGS
+    st->codecpar->channels       = di.channel_count;
+    st->codecpar->channel_layout = di.ffmpeg_channel_mask;
+FF_ENABLE_DEPRECATION_WARNINGS
+#endif
+
+    ret = write_extradata(st->codecpar, &di);
+    if (ret < 0)
+        return ret;
+
+    if (st->codecpar->sample_rate)
+        avpriv_set_pts_info(st, 64, 1, st->codecpar->sample_rate);
+
+    return 0;
+}
+
+static int read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    DTSUHDDemuxContext *dtsuhd = s->priv_data;
+    int64_t size, left;
+    int ret;
+
+    left = dtsuhd->data_end - avio_tell(s->pb);
+    size = FFMIN(left, DTSUHD_MAX_FRAME_SIZE);
+    if (size <= 0)
+        return AVERROR_EOF;
+
+    ret = av_get_packet(s->pb, pkt, size);
+    if (ret < 0)
+        return ret;
+
+    pkt->stream_index = 0;
+
+    return ret;
+}
+
+AVInputFormat ff_dtsuhd_demuxer = {
+    .name           = "dtsuhd",
+    .long_name      = NULL_IF_CONFIG_SMALL("DTS-UHD"),
+    .priv_data_size = sizeof(DTSUHDDemuxContext),
+    .read_probe     = probe,
+    .read_header    = read_header,
+    .read_packet    = read_packet,
+    .read_close     = read_close,
+    .flags          = AVFMT_GENERIC_INDEX,
+    .extensions     = "dtsx",
+    .raw_codec_id   = AV_CODEC_ID_DTSUHD,
+};
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index c95410f5c1..238c82b7ee 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -762,6 +762,24 @@ static int mov_write_esds_tag(AVIOContext *pb, MOVTrack *track) // Basic
     return update_size(pb, pos);
 }
 
+static int mov_write_udts_tag(AVIOContext *pb, MOVTrack *track)
+{
+    if (track->vos_len < 12) {
+        av_log(pb, AV_LOG_ERROR,
+               "Cannot write moov atom before DTS-UHD packets."
+               " Set the delay_moov flag to fix this.\n");
+        return AVERROR(EINVAL);
+    }
+
+    /* Write vos_data is udts box. */
+    if (memcmp(track->vos_data + 4, "udts", 4) == 0) {
+        avio_write(pb, track->vos_data, track->vos_len);
+        return track->vos_len;
+    }
+
+    return 0;
+}
+
 static int mov_pcm_le_gt16(enum AVCodecID codec_id)
 {
     return codec_id == AV_CODEC_ID_PCM_S24LE ||
@@ -1372,6 +1390,8 @@ static int mov_write_audio_tag(AVFormatContext *s, AVIOContext *pb, MOVMuxContex
         ret = mov_write_dops_tag(s, pb, track);
     else if (track->par->codec_id == AV_CODEC_ID_TRUEHD)
         ret = mov_write_dmlp_tag(s, pb, track);
+    else if (track->par->codec_id == AV_CODEC_ID_DTSUHD)
+        ret = mov_write_udts_tag(pb, track);
     else if (tag == MOV_MP4_IPCM_TAG || tag == MOV_MP4_FPCM_TAG) {
         if (track->par->ch_layout.nb_channels > 1)
             ret = mov_write_chnl_tag(s, pb, track);
@@ -2823,6 +2843,7 @@ static int mov_write_stbl_tag(AVFormatContext *s, AVIOContext *pb, MOVMuxContext
     if ((track->par->codec_type == AVMEDIA_TYPE_VIDEO ||
          track->par->codec_id == AV_CODEC_ID_TRUEHD ||
          track->par->codec_id == AV_CODEC_ID_MPEGH_3D_AUDIO ||
+         track->par->codec_id == AV_CODEC_ID_DTSUHD ||
          track->par->codec_tag == MKTAG('r','t','p',' ')) &&
         track->has_keyframes && track->has_keyframes < track->entry)
         mov_write_stss_tag(pb, track, MOV_SYNC_SAMPLE);
@@ -5739,6 +5760,14 @@ static void mov_parse_truehd_frame(AVPacket *pkt, MOVTrack *trk)
     return;
 }
 
+static void mov_parse_dtsuhd_frame(AVPacket *pkt, MOVTrack *trk)
+{
+    if (pkt->size > 4 && AV_RB32(pkt->data) == 0x40411BF2) {
+        trk->cluster[trk->entry].flags |= MOV_SYNC_SAMPLE;
+        trk->has_keyframes++;
+    }
+}
+
 static int mov_flush_fragment_interleaving(AVFormatContext *s, MOVTrack *track)
 {
     MOVMuxContext *mov = s->priv_data;
@@ -6391,6 +6420,8 @@ int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt)
         mov_parse_vc1_frame(pkt, trk);
     } else if (par->codec_id == AV_CODEC_ID_TRUEHD) {
         mov_parse_truehd_frame(pkt, trk);
+    } else if (par->codec_id == AV_CODEC_ID_DTSUHD) {
+        mov_parse_dtsuhd_frame(pkt, trk);
     } else if (pkt->flags & AV_PKT_FLAG_KEY) {
         if (mov->mode == MODE_MOV && par->codec_id == AV_CODEC_ID_MPEG2VIDEO &&
             trk->entry > 0) { // force sync sample for the first key frame
@@ -7850,6 +7881,7 @@ static const AVCodecTag codec_mp4_tags[] = {
     { AV_CODEC_ID_AC3,             MKTAG('a', 'c', '-', '3') },
     { AV_CODEC_ID_EAC3,            MKTAG('e', 'c', '-', '3') },
     { AV_CODEC_ID_DTS,             MKTAG('m', 'p', '4', 'a') },
+    { AV_CODEC_ID_DTSUHD,          MKTAG('d', 't', 's', 'x') },
     { AV_CODEC_ID_TRUEHD,          MKTAG('m', 'l', 'p', 'a') },
     { AV_CODEC_ID_FLAC,            MKTAG('f', 'L', 'a', 'C') },
     { AV_CODEC_ID_OPUS,            MKTAG('O', 'p', 'u', 's') },
diff --git a/libavformat/version.h b/libavformat/version.h
index de9cc8e31d..683184d5da 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
 
 #include "version_major.h"
 
-#define LIBAVFORMAT_VERSION_MINOR  20
+#define LIBAVFORMAT_VERSION_MINOR  21
 #define LIBAVFORMAT_VERSION_MICRO 100
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
-- 
2.17.1