From 13f4c832da01fdb6e87056c1760627ab1b44bbe8 Mon Sep 17 00:00:00 2001 From: glass Date: Fri, 2 Apr 2021 07:38:52 +0200 Subject: [PATCH] Add MP4 extraction of Dolby TrueHD samples Extract 16 access units per readSample call to align with what's done in MKV extraction. Signed-off-by: glass --- .../android/exoplayer2/audio/MlpUtil.java | 104 ++++++++++++++++++ .../exoplayer2/extractor/mp4/Atom.java | 6 + .../exoplayer2/extractor/mp4/AtomParsers.java | 20 +++- .../extractor/mp4/Mp4Extractor.java | 52 +++++++-- 4 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/audio/MlpUtil.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/MlpUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/MlpUtil.java new file mode 100644 index 0000000000..a8fa37e81f --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/MlpUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** Utility methods for parsing MLP frames, which are access units in MLP bitstreams. */ +public final class MlpUtil { + + /** a MLP stream can carry simultaneously multiple representations of the same audio : + * stereo as well as multichannel and object based immersive audio, + * so just consider stereo by default */ + private static final int CHANNEL_COUNT_2 = 2; + + /** + * Returns the MLP format given {@code data} containing the MLPSpecificBox according to + * dolbytruehdbitstreamswithintheisobasemediafileformat.pdf + * The reading position of {@code data} will be modified. + * + * @param data The MLPSpecificBox to parse. + * @param trackId The track identifier to set on the format. + * @param sampleRate The sample rate to be included in the format. + * @param language The language to set on the format. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @return The MLP format parsed from data in the header. + */ + public static Format parseMlpFormat( + ParsableByteArray data, String trackId, int sampleRate, + String language, @Nullable DrmInitData drmInitData) { + + return new Format.Builder() + .setId(trackId) + .setSampleMimeType(MimeTypes.AUDIO_TRUEHD) + .setChannelCount(CHANNEL_COUNT_2) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); + } + + private MlpUtil() {} + + /** + * The number of samples to store in each output chunk when rechunking TrueHD streams. The number + * of samples extracted from the container corresponding to one syncframe must be an integer + * multiple of this value. + */ + public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 16; + + /** + * Rechunks TrueHD sample data into groups of {@link #TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + public static class TrueHdSampleRechunker { + + private int sampleCount; + public long timeUs; + public @C.BufferFlags int flags; + public int sampleSize; + + public TrueHdSampleRechunker() { + reset(); + } + + public void reset() { + sampleCount = 0; + sampleSize = 0; + } + + /** Returns true when enough samples have been appended. */ + public boolean appendSampleMetadata(long timeUs, @C.BufferFlags int flags, int size) { + + if (sampleCount++ == 0) { + // This is the first sample in the chunk. + this.timeUs = timeUs; + this.flags = flags; + this.sampleSize = 0; + } + this.sampleSize += size; + if (sampleCount >= TRUEHD_RECHUNK_SAMPLE_COUNT) { + sampleCount = 0; + return true; + } + return false; + } + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index a85d928f04..bc8633acc8 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -152,6 +152,12 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dac4 = 0x64616334; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mlpa = 0x6d6c7061; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dmlp = 0x646d6c70; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dtsc = 0x64747363; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 067d53ebfe..e739694dca 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.AacUtil; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac4Util; +import com.google.android.exoplayer2.audio.MlpUtil; import com.google.android.exoplayer2.audio.OpusUtil; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.ExtractorUtil; @@ -962,6 +963,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3 || childAtomType == Atom.TYPE_ac_4 + || childAtomType == Atom.TYPE_mlpa || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse || childAtomType == Atom.TYPE_dtsh @@ -1312,14 +1314,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; parent.skipBytes(8); } - int channelCount; - int sampleRate; + int channelCount; + int sampleRate; + int sampleRate32 = 0; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; @Nullable String codecs = null; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); - parent.skipBytes(6); // sampleSize, compressionId, packetSize. + parent.skipBytes(6); // sampleSize, compressionId, packetSize. + + int pos = parent.getPosition(); + sampleRate32 = (int) parent.readUnsignedInt(); + + parent.setPosition(pos); sampleRate = parent.readUnsignedFixedPoint1616(); if (quickTimeSoundDescriptionVersion == 1) { @@ -1401,6 +1409,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.AUDIO_OPUS; } else if (atomType == Atom.TYPE_fLaC) { mimeType = MimeTypes.AUDIO_FLAC; + } else if (atomType == Atom.TYPE_mlpa) { + mimeType = MimeTypes.AUDIO_TRUEHD; } @Nullable List initializationData = null; @@ -1442,6 +1452,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; initializationData = ImmutableList.of(initializationDataBytes); } } + } else if (childAtomType == Atom.TYPE_dmlp) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = MlpUtil.parseMlpFormat(parent, Integer.toString(trackId), + sampleRate32, language, drmInitData); } else if (childAtomType == Atom.TYPE_dac3) { parent.setPosition(Atom.HEADER_SIZE + childPosition); out.format = diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index d542fa8545..11e11898e8 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac4Util; +import com.google.android.exoplayer2.audio.MlpUtil; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -501,11 +502,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs; durationUs = max(durationUs, trackDurationUs); Mp4Track mp4Track = - new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); + new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type), track.format.sampleMimeType); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + + if ((track.format.sampleMimeType != null) && (track.format.sampleMimeType.equals(MimeTypes.AUDIO_TRUEHD))) { + // TrueHD collates 16 source samples per output + maxInputSize = trackSampleTable.maximumSize * MlpUtil.TRUEHD_RECHUNK_SAMPLE_COUNT; + } + Format.Builder formatBuilder = track.format.buildUpon(); formatBuilder.setMaxInputSize(maxInputSize); if (track.type == C.TRACK_TYPE_VIDEO @@ -540,7 +547,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { } /** - * Attempts to extract the next sample in the current mdat atom for the specified track. + * Attempts to extract the next sample or the next 16 samples in case of Dolby TrueHD audio + * in the current mdat atom for the specified track. * *

Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in {@code * positionHolder}. @@ -632,12 +640,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleCurrentNalBytesRemaining -= writtenBytes; } } - trackOutput.sampleMetadata( - track.sampleTable.timestampsUs[sampleIndex], - track.sampleTable.flags[sampleIndex], - sampleSize, - 0, - null); + + track.sampleMetadata(sampleIndex, sampleSize, 0, null); + track.sampleIndex++; sampleTrackIndex = C.INDEX_UNSET; sampleBytesRead = 0; @@ -904,11 +909,40 @@ public final class Mp4Extractor implements Extractor, SeekMap { public final TrackOutput trackOutput; public int sampleIndex; + @Nullable public MlpUtil.TrueHdSampleRechunker trueHdSampleRechunker; - public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) { + public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput, @Nullable String mimeType) { this.track = track; this.sampleTable = sampleTable; this.trackOutput = trackOutput; + this.trueHdSampleRechunker = null; + + if ((mimeType != null) && mimeType.equals(MimeTypes.AUDIO_TRUEHD)) { + this.trueHdSampleRechunker = new MlpUtil.TrueHdSampleRechunker(); + } + } + + public void sampleMetadata( int sampleIndex, int sampleSize, int offset, + @Nullable TrackOutput.CryptoData cryptoData) { + + long timeUs = sampleTable.timestampsUs[sampleIndex]; + @C.BufferFlags int flags = sampleTable.flags[sampleIndex]; + + if (trueHdSampleRechunker != null) { + boolean fullChunk = trueHdSampleRechunker.appendSampleMetadata(timeUs,flags,sampleSize); + + if (fullChunk || (sampleIndex+1 == sampleTable.sampleCount)) { + timeUs = trueHdSampleRechunker.timeUs; + flags = trueHdSampleRechunker.flags; + sampleSize = trueHdSampleRechunker.sampleSize; + + trackOutput.sampleMetadata( timeUs, flags, sampleSize, offset, cryptoData); + trueHdSampleRechunker.reset(); + } + } else { + trackOutput.sampleMetadata( timeUs, flags, sampleSize, offset, cryptoData); + } } } + }