From 8e8e53c42d7a63ee5f6703d86902ebb102c10af5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 3 Jan 2018 09:18:11 -0800 Subject: [PATCH] Add support for Dolby TrueHD passthrough Issue: #2147 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180678595 --- RELEASENOTES.md | 2 + .../java/com/google/android/exoplayer2/C.java | 63 ++++----- .../android/exoplayer2/audio/Ac3Util.java | 52 ++++++- .../exoplayer2/audio/DefaultAudioSink.java | 15 +- .../extractor/mkv/MatroskaExtractor.java | 131 ++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 2 + 6 files changed, 211 insertions(+), 54 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 25e4e841e30..4679a0b3760 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,8 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Audio: Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 6a35c0c5e86..d6e61c12b18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -122,13 +122,22 @@ private C() {} */ public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; - /** - * Represents an audio encoding, or an invalid or unset value. - */ + /** Represents an audio encoding, or an invalid or unset value. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, - ENCODING_DTS, ENCODING_DTS_HD}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT, + ENCODING_AC3, + ENCODING_E_AC3, + ENCODING_DTS, + ENCODING_DTS_HD, + ENCODING_DOLBY_TRUEHD + }) public @interface Encoding {} /** @@ -138,46 +147,28 @@ private C() {} @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} - /** - * @see AudioFormat#ENCODING_INVALID - */ + /** @see AudioFormat#ENCODING_INVALID */ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; - /** - * @see AudioFormat#ENCODING_PCM_8BIT - */ + /** @see AudioFormat#ENCODING_PCM_8BIT */ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; - /** - * @see AudioFormat#ENCODING_PCM_16BIT - */ + /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; - /** - * PCM encoding with 24 bits per sample. - */ + /** PCM encoding with 24 bits per sample. */ public static final int ENCODING_PCM_24BIT = 0x80000000; - /** - * PCM encoding with 32 bits per sample. - */ + /** PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; - /** - * @see AudioFormat#ENCODING_PCM_FLOAT - */ + /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** - * @see AudioFormat#ENCODING_AC3 - */ + /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; - /** - * @see AudioFormat#ENCODING_E_AC3 - */ + /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; - /** - * @see AudioFormat#ENCODING_DTS - */ + /** @see AudioFormat#ENCODING_DTS */ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; - /** - * @see AudioFormat#ENCODING_DTS_HD - */ + /** @see AudioFormat#ENCODING_DTS_HD */ public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; + /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */ + public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; /** * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e9ffab7acec..5797e737409 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -27,9 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; -/** - * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams. - */ +/** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */ public final class Ac3Util { /** @@ -93,6 +91,17 @@ private Ac3SyncFrameInfo(String mimeType, int streamType, int channelCount, int } + /** + * 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 = 8; + /** + * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count. + */ + public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12; + /** * The number of new samples per (E-)AC-3 audio block. */ @@ -441,6 +450,43 @@ public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); } + /** + * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the + * buffer is not the start of a syncframe. + * + * @param syncframe The bytes from which to read the syncframe. Must be at least {@link + * #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't + * contain the start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { + // TODO: Link to specification if available. + if (syncframe[4] != (byte) 0xF8 + || syncframe[5] != (byte) 0x72 + || syncframe[6] != (byte) 0x6F + || syncframe[7] != (byte) 0xBA) { + return 0; + } + return 40 << (syncframe[8] & 7); + } + + /** + * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer + * is not the start of a syncframe. The buffer's position is not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least + * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the + * start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) { + // TODO: Link to specification if available. + if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) { + return 0; + } + return 40 << (buffer.get(buffer.position() + 8) & 0x07); + } + private static int getAc3SyncframeSize(int fscod, int frmsizecod) { int halfFrmsizecod = frmsizecod / 2; if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index b9a0b8236fa..e3bf72c541a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -446,9 +446,12 @@ public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { + } else if (outputEncoding == C.ENCODING_DTS) { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); + } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ { + // HD passthrough requires a larger buffer to avoid underrun. + bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); } } bufferSizeUs = @@ -580,6 +583,13 @@ public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); + if (framesPerEncodedSample == 0) { + // We still don't know the number of frames per sample, so drop the buffer. + // For TrueHD this can occur after some seek operations, as not every sample starts with + // a syncframe header. If we chunked samples together so the extracted samples always + // started with a syncframe header, the chunks would be too large. + return true; + } } if (drainingPlaybackParameters != null) { @@ -1225,6 +1235,9 @@ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffe return Ac3Util.getAc3SyncframeAudioSampleCount(); } else if (encoding == C.ENCODING_E_AC3) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { + return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT; } else { throw new IllegalStateException("Unexpected audio encoding: " + encoding); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 4b0bbda2756..0eb7009c479 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -32,6 +34,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -413,6 +416,9 @@ public void seek(long position, long timeUs) { reader.reset(); varintReader.reset(); resetSample(); + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).reset(); + } } @Override @@ -431,7 +437,13 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce return Extractor.RESULT_SEEK; } } - return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT; + if (!continueReading) { + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).outputPendingSampleMetadata(); + } + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; } /* package */ int getElementType(int id) { @@ -1077,14 +1089,26 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce } private void commitSampleToOutput(Track track, long timeUs) { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY); - } else if (CODEC_ID_ASS.equals(track.codecId)) { - commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET, - SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY); + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + commitSubtitleSample( + track, + SUBRIP_TIMECODE_FORMAT, + SUBRIP_PREFIX_END_TIMECODE_OFFSET, + SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SUBRIP_TIMECODE_EMPTY); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + commitSubtitleSample( + track, + SSA_TIMECODE_FORMAT, + SSA_PREFIX_END_TIMECODE_OFFSET, + SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SSA_TIMECODE_EMPTY); + } + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); sampleRead = true; resetSample(); } @@ -1251,6 +1275,10 @@ private void writeSampleData(ExtractorInput input, Track track, int size) } } } else { + if (track.trueHdSampleRechunker != null) { + Assertions.checkState(sampleStrippedBytes.limit() == 0); + track.trueHdSampleRechunker.startSample(input, blockFlags, size); + } while (sampleBytesRead < size) { readToOutput(input, output, size - sampleBytesRead); } @@ -1510,7 +1538,70 @@ public void binaryElement(int id, int contentsSize, ExtractorInput input) throws IOException, InterruptedException { MatroskaExtractor.this.binaryElement(id, contentsSize, input); } + } + + /** + * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + private static final class TrueHdSampleRechunker { + + private final byte[] syncframePrefix; + + private boolean foundSyncframe; + private int sampleCount; + private int chunkSize; + private long timeUs; + private @C.BufferFlags int blockFlags; + + public TrueHdSampleRechunker() { + syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; + } + + public void reset() { + foundSyncframe = false; + } + + public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) + throws IOException, InterruptedException { + if (!foundSyncframe) { + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) { + return; + } + foundSyncframe = true; + sampleCount = 0; + } + if (sampleCount == 0) { + // This is the first sample in the chunk, so reset the block flags and chunk size. + this.blockFlags = blockFlags; + chunkSize = 0; + } + chunkSize += size; + } + public void sampleMetadata(Track track, long timeUs) { + if (!foundSyncframe) { + return; + } + if (sampleCount++ == 0) { + // This is the first sample in the chunk, so update the timestamp. + this.timeUs = timeUs; + } + if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + // We haven't read enough samples to output a chunk. + return; + } + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + + public void outputPendingSampleMetadata(Track track) { + if (foundSyncframe && sampleCount > 0) { + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + } } private static final class Track { @@ -1573,6 +1664,7 @@ private static final class Track { public int sampleRate = 8000; public long codecDelayNs = 0; public long seekPreRollNs = 0; + @Nullable public TrueHdSampleRechunker trueHdSampleRechunker; // Text elements. public boolean flagForced; @@ -1583,9 +1675,7 @@ private static final class Track { public TrackOutput output; public int nalUnitLengthFieldLength; - /** - * Initializes the track with an output. - */ + /** Initializes the track with an output. */ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { String mimeType; int maxInputSize = Format.NO_VALUE; @@ -1669,6 +1759,7 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE break; case CODEC_ID_TRUEHD: mimeType = MimeTypes.AUDIO_TRUEHD; + trueHdSampleRechunker = new TrueHdSampleRechunker(); break; case CODEC_ID_DTS: case CODEC_ID_DTS_EXPRESS: @@ -1786,9 +1877,21 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE this.output.format(format); } - /** - * Returns the HDR Static Info as defined in CTA-861.3. - */ + /** Forces any pending sample metadata to be flushed to the output. */ + public void outputPendingSampleMetadata() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.outputPendingSampleMetadata(this); + } + } + + /** Resets any state stored in the track in response to a seek. */ + public void reset() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.reset(); + } + } + + /** Returns the HDR Static Info as defined in CTA-861.3. */ private byte[] getHdrStaticInfo() { // Are all fields present. if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 8307e998a09..3e65a754e24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -264,6 +264,8 @@ public static int getTrackType(String mimeType) { return C.ENCODING_DTS; case MimeTypes.AUDIO_DTS_HD: return C.ENCODING_DTS_HD; + case MimeTypes.AUDIO_TRUEHD: + return C.ENCODING_DOLBY_TRUEHD; default: return C.ENCODING_INVALID; }