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 <glass@dolby.com>
This commit is contained in:
glass 2021-04-02 07:38:52 +02:00 committed by glass
parent 2138bfb396
commit 13f4c832da
4 changed files with 170 additions and 12 deletions

View File

@ -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;
}
}
}

View File

@ -152,6 +152,12 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants") @SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_dac4 = 0x64616334; 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") @SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_dtsc = 0x64747363; public static final int TYPE_dtsc = 0x64747363;

View File

@ -28,6 +28,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.AacUtil; import com.google.android.exoplayer2.audio.AacUtil;
import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.audio.Ac4Util; 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.audio.OpusUtil;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.ExtractorUtil; 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_ac_3
|| childAtomType == Atom.TYPE_ec_3 || childAtomType == Atom.TYPE_ec_3
|| childAtomType == Atom.TYPE_ac_4 || childAtomType == Atom.TYPE_ac_4
|| childAtomType == Atom.TYPE_mlpa
|| childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtsc
|| childAtomType == Atom.TYPE_dtse || childAtomType == Atom.TYPE_dtse
|| childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsh
@ -1314,12 +1316,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int channelCount; int channelCount;
int sampleRate; int sampleRate;
int sampleRate32 = 0;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
@Nullable String codecs = null; @Nullable String codecs = null;
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
channelCount = parent.readUnsignedShort(); 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(); sampleRate = parent.readUnsignedFixedPoint1616();
if (quickTimeSoundDescriptionVersion == 1) { if (quickTimeSoundDescriptionVersion == 1) {
@ -1401,6 +1409,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
mimeType = MimeTypes.AUDIO_OPUS; mimeType = MimeTypes.AUDIO_OPUS;
} else if (atomType == Atom.TYPE_fLaC) { } else if (atomType == Atom.TYPE_fLaC) {
mimeType = MimeTypes.AUDIO_FLAC; mimeType = MimeTypes.AUDIO_FLAC;
} else if (atomType == Atom.TYPE_mlpa) {
mimeType = MimeTypes.AUDIO_TRUEHD;
} }
@Nullable List<byte[]> initializationData = null; @Nullable List<byte[]> initializationData = null;
@ -1442,6 +1452,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
initializationData = ImmutableList.of(initializationDataBytes); 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) { } else if (childAtomType == Atom.TYPE_dac3) {
parent.setPosition(Atom.HEADER_SIZE + childPosition); parent.setPosition(Atom.HEADER_SIZE + childPosition);
out.format = out.format =

View File

@ -30,6 +30,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.Ac4Util; 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.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; 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; track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs;
durationUs = max(durationUs, trackDurationUs); durationUs = max(durationUs, trackDurationUs);
Mp4Track mp4Track = 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. // 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. // Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10; 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(); Format.Builder formatBuilder = track.format.buildUpon();
formatBuilder.setMaxInputSize(maxInputSize); formatBuilder.setMaxInputSize(maxInputSize);
if (track.type == C.TRACK_TYPE_VIDEO 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.
* *
* <p>Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in {@code * <p>Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in {@code
* positionHolder}. * positionHolder}.
@ -632,12 +640,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
sampleCurrentNalBytesRemaining -= writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes;
} }
} }
trackOutput.sampleMetadata(
track.sampleTable.timestampsUs[sampleIndex], track.sampleMetadata(sampleIndex, sampleSize, 0, null);
track.sampleTable.flags[sampleIndex],
sampleSize,
0,
null);
track.sampleIndex++; track.sampleIndex++;
sampleTrackIndex = C.INDEX_UNSET; sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0; sampleBytesRead = 0;
@ -904,11 +909,40 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public final TrackOutput trackOutput; public final TrackOutput trackOutput;
public int sampleIndex; 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.track = track;
this.sampleTable = sampleTable; this.sampleTable = sampleTable;
this.trackOutput = trackOutput; 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);
} }
} }
} }
}