From a9720457d8b134df973240cb0abc20b652e34ee9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Jan 2016 04:59:17 -0800 Subject: [PATCH] Add support for playing DTS in HLS. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=112774678 --- .../android/exoplayer/audio/AudioTrack.java | 6 +- .../exoplayer/extractor/ts/DtsReader.java | 166 ++++++++++++++++++ .../exoplayer/extractor/ts/TsExtractor.java | 5 + .../android/exoplayer/util/Ac3Util.java | 4 +- .../android/exoplayer/util/DtsUtil.java | 116 ++++++++++++ 5 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index ea50a18603..34b5db935a 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.audio; import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Ac3Util; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.DtsUtil; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; @@ -965,10 +966,7 @@ public final class AudioTrack { private static int getFramesPerEncodedSample(int encoding, ByteBuffer buffer) { if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { - // Calculate the sample size in frames as per ETSI TS 102 114 F.3.2.1. - int nblks = ((buffer.get(buffer.position() + 4) & 0x01) << 6) - | ((buffer.get(buffer.position() + 5) & 0xFC) >> 2); - return (nblks + 1) * 32; + return DtsUtil.parseDtsAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC3) { return Ac3Util.getAc3SyncframeAudioSampleCount(); } else if (encoding == C.ENCODING_E_AC3) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java new file mode 100644 index 0000000000..d363c0de8b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/DtsReader.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2014 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.exoplayer.extractor.ts; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.DtsUtil; +import com.google.android.exoplayer.util.ParsableByteArray; + +/** + * Parses a continuous DTS byte stream and extracts individual samples. + */ +/* package */ final class DtsReader extends ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 15; + private static final int SYNC_VALUE = 0x7FFE8001; + private static final int SYNC_VALUE_SIZE = 4; + + private final ParsableByteArray headerScratchBytes; + + private int state; + private int bytesRead; + + // Used to find the header. + private int syncBytes; + + // Used when parsing the header. + private long sampleDurationUs; + private MediaFormat mediaFormat; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for DTS elementary streams. + * + * @param output Track output for extracted samples. + */ + public DtsReader(TrackOutput output) { + super(output); + headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); + headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); + headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); + headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF); + headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF); + state = STATE_FINDING_SYNC; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + syncBytes = 0; + } + + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + if (startOfPacket) { + timeUs = pesTimeUs; + } + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + bytesRead = SYNC_VALUE_SIZE; + state = STATE_READING_HEADER; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, HEADER_SIZE); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately + * follows it. If SYNC was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if SYNC was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + syncBytes <<= 8; + syncBytes |= pesBuffer.readUnsignedByte(); + if (syncBytes == SYNC_VALUE) { + syncBytes = 0; + return true; + } + } + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + byte[] frameData = headerScratchBytes.data; + if (mediaFormat == null) { + mediaFormat = DtsUtil.parseDtsFormat(frameData, null, C.UNKNOWN_TIME_US, null); + output.format(mediaFormat); + } + sampleSize = DtsUtil.getDtsFrameSize(frameData); + // In this class a sample is an access unit (frame in DTS), but the MediaFormat sample rate + // specifies the number of PCM audio samples per second. + sampleDurationUs = (int) (C.MICROS_PER_SECOND + * DtsUtil.parseDtsAudioSampleCount(frameData) / mediaFormat.sampleRate); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 4ce7cdf071..bb8351ea5a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -47,6 +47,7 @@ public final class TsExtractor implements Extractor { private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_AC3 = 0x81; private static final int TS_STREAM_TYPE_DTS = 0x8A; + private static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; private static final int TS_STREAM_TYPE_E_AC3 = 0x87; private static final int TS_STREAM_TYPE_H262 = 0x02; private static final int TS_STREAM_TYPE_H264 = 0x1B; @@ -345,6 +346,10 @@ public final class TsExtractor implements Extractor { case TS_STREAM_TYPE_E_AC3: pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_E_AC3), true); break; + case TS_STREAM_TYPE_DTS: + case TS_STREAM_TYPE_HDMV_DTS: + pesPayloadReader = new DtsReader(output.track(TS_STREAM_TYPE_DTS)); + break; case TS_STREAM_TYPE_H262: pesPayloadReader = new H262Reader(output.track(TS_STREAM_TYPE_H262)); break; diff --git a/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java b/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java index 8642af4409..c65e8b3ffc 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java @@ -235,8 +235,6 @@ public final class Ac3Util { } } - private Ac3Util() { - // Prevent instantiation. - } + private Ac3Util() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java b/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java new file mode 100644 index 0000000000..5fa6e3e6e4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 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.exoplayer.util; + +import com.google.android.exoplayer.MediaFormat; + +import java.nio.ByteBuffer; + +/** + * Utility methods for parsing DTS frames. + */ +public final class DtsUtil { + + /** + * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4. + */ + private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, + 7, 8, 8}; + + /** + * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5. + */ + private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1, + 11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1}; + + /** + * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7. + */ + private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256, + 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816, + 2823, 2944, 3072, 3840, 4096, 6144, 7680}; + + private static final ParsableBitArray SCRATCH_BITS = new ParsableBitArray(); + + /** + * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114 + * subsections 5.3/5.4. + *

+ * This method may only be called from one thread at a time. + * + * @param frame The DTS frame to parse. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The DTS format parsed from data in the header. + */ + public static MediaFormat parseDtsFormat(byte[] frame, String trackId, long durationUs, + String language) { + ParsableBitArray frameBits = SCRATCH_BITS; + frameBits.reset(frame); + frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE + int amode = frameBits.readBits(6); + int channelCount = CHANNELS_BY_AMODE[amode]; + int sfreq = frameBits.readBits(4); + int sampleRate = SAMPLE_RATE_BY_SFREQ[sfreq]; + int rate = frameBits.readBits(5); + int bitrate = rate >= TWICE_BITRATE_KBPS_BY_RATE.length ? MediaFormat.NO_VALUE + : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2; + frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF + channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_DTS, bitrate, + MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, language); + } + + /** + * Returns the number of audio samples represented by the given DTS frame. + * + * @param data The frame to parse. + * @return The number of audio samples represented by the frame. + */ + public static int parseDtsAudioSampleCount(byte[] data) { + // See ETSI TS 102 114 subsection 5.4.1. + int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + return (nblks + 1) * 32; + } + + /** + * Like {@link #parseDtsAudioSampleCount(byte[])} but reads from a byte buffer. The buffer + * position is not modified. + */ + public static int parseDtsAudioSampleCount(ByteBuffer data) { + // See ETSI TS 102 114 subsection 5.4.1. + int position = data.position(); + int nblks = ((data.get(position + 4) & 0x01) << 6) + | ((data.get(position + 5) & 0xFC) >> 2); + return (nblks + 1) * 32; + } + + /** + * Returns the size in bytes of the given DTS frame. + * + * @param data The frame to parse. + * @return The frame's size in bytes. + */ + public static int getDtsFrameSize(byte[] data) { + return (((data[5] & 0x02) << 12) + | ((data[6] & 0xFF) << 4) + | ((data[7] & 0xF0) >> 4)) + 1; + } + + private DtsUtil() {} + +}