diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java new file mode 100644 index 0000000000..f3c42a4b5b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java @@ -0,0 +1,162 @@ +/* + * 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.Ac3Util; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +/** + * Parses a continuous AC-3 byte stream and extracts individual samples. + */ +/* package */ final class Ac3Reader 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 = 8; + + private final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + + private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWas0B; + + // Used when parsing the header. + private long frameDurationUs; + private MediaFormat mediaFormat; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + public Ac3Reader(TrackOutput output) { + super(output); + headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.getData()); + state = STATE_FINDING_SYNC; + } + + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWas0B = false; + } + + @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)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = 0x0B; + headerScratchBytes.data[1] = 0x77; + bytesRead = 2; + } + 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 += frameDurationUs; + 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 word, advancing the position to the byte that immediately follows it. + * If a sync word was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if a sync word position was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWas0B) { + lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B; + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + if (secondByte == 0x77) { + lastByteWas0B = false; + return true; + } else { + lastByteWas0B = secondByte == 0x0B; + } + } + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + if (mediaFormat == null) { + headerScratchBits.setPosition(0); + mediaFormat = Ac3Util.parseFrameAc3Format(headerScratchBits); + output.format(mediaFormat); + } + headerScratchBits.setPosition(0); + sampleSize = Ac3Util.parseFrameSize(headerScratchBits); + frameDurationUs = (int) (1000000L * 8 * sampleSize / mediaFormat.bitrate); + } + +} 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 432195e6a6..34c1251c11 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 @@ -40,6 +40,8 @@ public final class TsExtractor implements Extractor { private static final int TS_PAT_PID = 0; private static final int TS_STREAM_TYPE_AAC = 0x0F; + private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81; + private static final int TS_STREAM_TYPE_DVB_AC3 = 0x06; private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 @@ -262,11 +264,14 @@ public final class TsExtractor implements Extractor { case TS_STREAM_TYPE_AAC: pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC)); break; + case TS_STREAM_TYPE_ATSC_AC3: + case TS_STREAM_TYPE_DVB_AC3: + pesPayloadReader = new Ac3Reader(output.track(streamType)); + break; case TS_STREAM_TYPE_H264: SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608)); streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader); - pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), - seiReader); + pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader); break; case TS_STREAM_TYPE_ID3: pesPayloadReader = id3Reader; 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 new file mode 100644 index 0000000000..91059832f2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java @@ -0,0 +1,140 @@ +/* + * 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.util.Collections; + +/** + * Utility methods for parsing AC-3 headers. + */ +public final class Ac3Util { + + /** Sample rates, indexed by fscod. */ + private static final int[] SAMPLE_RATES = new int[] {48000, 44100, 32000}; + /** Channel counts, indexed by acmod. */ + private static final int[] CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; + /** Nominal bitrates in kbps, indexed by bit_rate_code. */ + private static final int[] BITRATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, + 224, 256, 320, 384, 448, 512, 576, 640}; + /** 16-bit words per sync frame, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) */ + private static final int[] FRMSIZECOD_TO_FRAME_SIZE_44_1 = new int[] {69, 87, 104, 121, 139, 174, + 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + + /** + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to + * ETSI TS 102 366 Annex F. + */ + public static MediaFormat parseAnnexFAc3Format(ParsableByteArray data) { + // fscod (sample rate code) + int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + int sampleRate = SAMPLE_RATES[fscod]; + int nextByte = data.readUnsignedByte(); + // Map acmod (audio coding mode) onto a channel count. + int channelCount = CHANNEL_COUNTS[(nextByte & 0x38) >> 3]; + // lfeon (low frequency effects on) + if ((nextByte & 0x04) != 0) { + channelCount++; + } + // Map bit_rate_code onto a bitrate in bit/s. + int bitrate = BITRATES[((nextByte & 0x03) << 3) + (data.readUnsignedByte() >> 5)] * 1000; + return MediaFormat.createAudioFormat(MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, channelCount, sampleRate, bitrate, Collections.emptyList()); + } + + /** + * Returns the AC-3 format given {@code data} containing the EC3SpecificBox according to + * ETSI TS 102 366 Annex F. + */ + public static MediaFormat parseAnnexFEAc3Format(ParsableByteArray data) { + data.skipBytes(2); // Skip data_rate and num_ind_sub. + + // Read only the first substream. + // TODO: Read later substreams? + // fscod (sample rate code) + int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + int sampleRate = SAMPLE_RATES[fscod]; + int nextByte = data.readUnsignedByte(); + // Map acmod (audio coding mode) onto a channel count. + int channelCount = CHANNEL_COUNTS[(nextByte & 0x0E) >> 1]; + // lfeon (low frequency effects on) + if ((nextByte & 0x01) != 0) { + channelCount++; + } + return MediaFormat.createAudioFormat(MimeTypes.AUDIO_EC3, MediaFormat.NO_VALUE, + channelCount, sampleRate, Collections.emptyList()); + } + + /** + * Returns the AC-3 format given {@code data} containing the frame header starting from the sync + * word. + * + * @param data Data to parse, positioned at the start of the syncword. + * @return AC-3 format parsed from data in the header. + */ + public static MediaFormat parseFrameAc3Format(ParsableBitArray data) { + // Skip syncword and crc1. + data.skipBits(4 * 8); + + int fscod = data.readBits(2); + int frmsizecod = data.readBits(6); + data.skipBits(8); // bsid (5 bits) + bsmod (3 bits) + int acmod = data.readBits(3); + if ((acmod & 0x01) != 0 && acmod != 1) { + data.skipBits(2); // cmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(2); // surmixlev + } + if (acmod == 0x02) { + data.skipBits(2); // dsurmod + } + boolean lfeon = data.readBit(); + return MediaFormat.createAudioFormat(MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, CHANNEL_COUNTS[acmod] + (lfeon ? 1 : 0), SAMPLE_RATES[fscod], + BITRATES[frmsizecod / 2] * 1000, Collections.emptyList()); + } + + /** + * Returns the AC-3 frame size in bytes given {@code data} containing the frame header starting + * from the sync word. + * + * @param data Data to parse, positioned at the start of the syncword. + * @return The frame size parsed from data in the header. + */ + public static int parseFrameSize(ParsableBitArray data) { + // Skip syncword and crc1. + data.skipBits(4 * 8); + + int fscod = data.readBits(2); + int frmsizecod = data.readBits(6); + int sampleRate = SAMPLE_RATES[fscod]; + int bitrate = BITRATES[frmsizecod / 2]; + if (sampleRate == 32000) { + return 6 * bitrate; + } else if (sampleRate == 44100) { + return 2 * (FRMSIZECOD_TO_FRAME_SIZE_44_1[frmsizecod / 2] + (frmsizecod % 2)); + } else { // sampleRate == 48000 + return 4 * bitrate; + } + } + + private Ac3Util() { + // Prevent instantiation. + } + +}