Add support for Dolby TrueHD passthrough

Issue: #2147

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=180678595
This commit is contained in:
andrewlewis 2018-01-03 09:18:11 -08:00 committed by Oliver Woodman
parent 7314e9bddc
commit 8e8e53c42d
6 changed files with 211 additions and 54 deletions

View file

@ -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 ###

View file

@ -122,13 +122,22 @@ public final class 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 @@ public final class 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

View file

@ -27,9 +27,7 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
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 @@ public final class Ac3Util {
}
/**
* 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 final class Ac3Util {
: 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

View file

@ -446,9 +446,12 @@ public final class DefaultAudioSink implements AudioSink {
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 final class DefaultAudioSink implements AudioSink {
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 @@ public final class DefaultAudioSink implements AudioSink {
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);
}

View file

@ -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.MpegAudioHeader;
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 final class MatroskaExtractor implements Extractor {
reader.reset();
varintReader.reset();
resetSample();
for (int i = 0; i < tracks.size(); i++) {
tracks.valueAt(i).reset();
}
}
@Override
@ -431,7 +437,13 @@ public final class MatroskaExtractor implements Extractor {
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 final class MatroskaExtractor implements Extractor {
}
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 @@ public final class MatroskaExtractor implements Extractor {
}
}
} 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 final class MatroskaExtractor implements Extractor {
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 @@ public final class MatroskaExtractor implements Extractor {
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 @@ public final class MatroskaExtractor implements Extractor {
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 final class MatroskaExtractor implements Extractor {
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 final class MatroskaExtractor implements Extractor {
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

View file

@ -264,6 +264,8 @@ public final class MimeTypes {
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;
}