mirror of
https://github.com/samsonjs/media.git
synced 2026-04-07 11:35:46 +00:00
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:
parent
7314e9bddc
commit
8e8e53c42d
6 changed files with 211 additions and 54 deletions
|
|
@ -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 ###
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue