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 fac9818d9ec..315551fa62a 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 @@ -143,8 +143,8 @@ private C() {} * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link - * #ENCODING_E_AC3}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link - * #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_E_AC3}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD}, {@link + * #ENCODING_DOLBY_TRUEHD} or {@link #ENCODING_AC4}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -162,7 +162,8 @@ private C() {} ENCODING_E_AC3, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD + ENCODING_DOLBY_TRUEHD, + ENCODING_AC4 }) public @interface Encoding {} @@ -212,6 +213,8 @@ private C() {} 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#ENCODING_AC4 */ + public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; /** * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java new file mode 100644 index 00000000000..9b845a14d43 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2018 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; + +/** + * Utility methods for parsing AC-4 frames, which are access units in AC-4 bitstreams. + */ +public final class Ac4Util { + + /** + * Holds sample format information as presented by a syncframe header. + */ + public static final class SyncFrameInfo { + + /** + * The sample mime type of the bitstream is {@link MimeTypes#AUDIO_AC4}. + */ + public final String mimeType; + /** + * The bitstream version. + */ + public final int bitstreamVersion; + /** + * The audio sampling rate in Hz. + */ + public final int sampleRate; + /** + * The number of audio channels + */ + public final int channelCount; + /** + * The size of the frame. + */ + public final int frameSize; + /** + * Number of audio samples in the frame. + */ + public final int sampleCount; + + private SyncFrameInfo( + String mimeType, + int bitstreamVersion, + int channelCount, + int sampleRate, + int frameSize, + int sampleCount) { + this.mimeType = mimeType; + this.bitstreamVersion = bitstreamVersion; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.frameSize = frameSize; + this.sampleCount = sampleCount; + } + } + + /** + * The channel count of AC-4 stream. + */ + // TODO: Parse AC-4 stream channel count. + public static final int CHANNEL_COUNT_2 = 2; + /** + * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full + * header size. + */ + public static final int HEADER_SIZE_FOR_PARSER = 16; + /** + * Number of audio samples in the frame. Defined in IEC61937-14:2017 table 5 and 6. This table + * provides the number of samples per frame at the playback sampling frequency of 48kHz. For + * 44.1kHz, only frame_rate_index(13) is valid and corresponding sample count is 2048. + */ + private static final int[] SAMPLE_COUNT = new int[] { + /* [ 0] 23.976 fps */ 2002, + /* [ 1] 24 fps */ 2000, + /* [ 2] 25 fps */ 1920, + /* [ 3] 29.97 fps */ 1601, // 1601 | 1602 | 1601 | 1602 | 1602 + /* [ 4] 30 fps */ 1600, + /* [ 5] 47.95 fps */ 1001, + /* [ 6] 48 fps */ 1000, + /* [ 7] 50 fps */ 960, + /* [ 8] 59.94 fps */ 800, // 800 | 801 | 801 | 801 | 801 + /* [ 9] 60 fps */ 800, + /* [10] 100 fps */ 480, + /* [11] 119.88 fps */ 400, // 400 | 400 | 401 | 400 | 401 + /* [12] 120 fps */ 400, + /* [13] 23.438 fps */ 2048 + }; + + /** + * Returns the AC-4 format given {@code data} containing the AC4SpecificBox according to ETSI TS + * 103 190-1 Annex E. The reading position of {@code data} will be modified. + * + * @param data The AC4SpecificBox to parse. + * @param trackId The track identifier to set on the format. + * @param language The language to set on the format. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @return The AC-4 format parsed from data in the header. + */ + public static Format parseAc4AnnexEFormat( + ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + data.skipBytes(1); + int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; + return Format.createAudioSampleFormat( + trackId, + MimeTypes.AUDIO_AC4, + null, + Format.NO_VALUE, + Format.NO_VALUE, + CHANNEL_COUNT_2, + sampleRate, + null, + drmInitData, + 0, + language); + } + + private static int readVariableBits(ParsableBitArray data, int nbits) { + int value = 0; + while (true) { + int moreBits; + value += data.readBits(nbits); + moreBits = data.readBits(1); + if (moreBits == 0) + break; + value++; + value <<= nbits; + } + return value; + } + + /** + * Returns AC-4 format information given {@code data} containing a syncframe. The reading + * position of {@code data} will be modified. + * + * @param data The data to parse, positioned at the start of the syncframe. + * @return The AC-4 format data parsed from the header. + */ + public static SyncFrameInfo parseAc4SyncframeInfo(ParsableBitArray data) { + int headerSize = 0; + int syncWord = data.readBits(16); + headerSize += 2; + int frameSize = data.readBits(16); + headerSize += 2; + if (frameSize == 0xFFFF) { + frameSize = data.readBits(24); + headerSize += 3; + } + frameSize += headerSize; + if (syncWord == 0xAC41) { + frameSize += 2; + } + int bitstreamVersion = data.readBits(2); + if (bitstreamVersion == 3){ + bitstreamVersion += readVariableBits(data, 2); + } + int sequenceCounter = data.readBits(10); + if (data.readBits(1) == 1) { + if (data.readBits(3) > 0){ + data.skipBits(2); + } + } + int sampleRate = (data.readBits(1) == 1) ? 48000 : 44100; + int frameRateIndex = data.readBits(4); + int sampleCount = 0; + if (sampleRate == 44100 && frameRateIndex == 13) { + sampleCount = SAMPLE_COUNT[frameRateIndex]; + } else if (sampleRate == 48000 && frameRateIndex < SAMPLE_COUNT.length) { + sampleCount = SAMPLE_COUNT[frameRateIndex]; + switch (sequenceCounter % 5) { + case 1: + case 3: + if (frameRateIndex == 3 || frameRateIndex == 8) { + sampleCount++; + } + break; + case 2: + if (frameRateIndex == 8 || frameRateIndex == 11) { + sampleCount++; + } + break; + case 4: + if (frameRateIndex == 3 || frameRateIndex == 8 || frameRateIndex == 11) { + sampleCount++; + } + break; + default: + break; + } + } + return new SyncFrameInfo( + MimeTypes.AUDIO_AC4, bitstreamVersion, CHANNEL_COUNT_2, sampleRate, frameSize, sampleCount); + } + + /** + * Returns the size in bytes of the given AC-4 syncframe. + * + * @param data The syncframe to parse. + * @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid. + */ + public static int parseAc4SyncframeSize(byte[] data, int syncBytes) { + if (data.length < 7) { + return C.LENGTH_UNSET; + } + int headerSize = 2; // syncword + int frameSize = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); + headerSize += 2; + if (frameSize == 0xFFFF) { + frameSize = ((data[4] & 0xFF) << 16) | ((data[5] & 0xFF) << 8) | (data[6] & 0xFF); + headerSize += 3; + } + if (syncBytes == 0xAC41) { + headerSize += 2; + } + frameSize += headerSize; + return frameSize; + } + + /** + * Reads the number of audio samples represented by the given AC-4 syncframe. The buffer's + * position is not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. + * @return The number of audio samples represented by the syncframe. + */ + public static int parseAc4SyncframeAudioSampleCount(ByteBuffer buffer) { + byte[] bufferBytes = new byte[HEADER_SIZE_FOR_PARSER]; + buffer.get(bufferBytes); + ParsableBitArray data = new ParsableBitArray(bufferBytes); + SyncFrameInfo ac4SyncframeInfo = parseAc4SyncframeInfo(data); + return ac4SyncframeInfo.sampleCount; + } + + /** + * Create AC-4 sample header, which includes AC-4 syncword and frame size. + * + * @param sampleSize The size of AC-4 sample. + * @return The AC-4 sample header byte array. + */ + public static ParsableByteArray getAc4SampleHeader(int sampleSize) { + // Add AC-4 Syncword 0xAC40 and frame size to create AC-4 syncframe + // ETSI TS 103 190-1 V1.3.1, Annex G + byte[] ac4SampleHeader = new byte[] {(byte)0xAC, 0x40, (byte)0xFF, (byte)0xFF, + (byte)(sampleSize >> 16 & 0xFF), (byte)(sampleSize >> 8 & 0xFF), (byte)(sampleSize & 0xFF)}; + return new ParsableByteArray(ac4SampleHeader); + } + + private Ac4Util() {} + +} 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 fc515dbdb34..686e2365fd0 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 @@ -1202,6 +1202,8 @@ private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) return 18000 * 1000 / 8; case C.ENCODING_DOLBY_TRUEHD: return 24500 * 1000 / 8; + case C.ENCODING_AC4: + return 2688 * 1000 / 8; case C.ENCODING_INVALID: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_24BIT: @@ -1229,6 +1231,8 @@ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffe ? 0 : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + } else if (encoding == C.ENCODING_AC4) { + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); } else { throw new IllegalStateException("Unexpected audio encoding: " + encoding); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 49c391c4cc8..f9749450c3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -787,6 +787,11 @@ protected MediaFormat getMediaFormat( mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } + if (MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + // Some devices handle AC-4 raw frame by default. + // Need to notify the codec to handle AC-4 sync frame. + mediaFormat.setInteger("ac4-is-sync", 1); + } return mediaFormat; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 54bb617c58c..14db0b2ae1b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.extractor.ogg.OggExtractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.PsExtractor; @@ -206,7 +207,7 @@ public synchronized DefaultExtractorsFactory setTsExtractorFlags( @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 12 : 13]; + Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14]; extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[2] = new Mp4Extractor(mp4Flags); @@ -235,9 +236,10 @@ public synchronized Extractor[] createExtractors() { | (constantBitrateSeekingEnabled ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING : 0)); + extractors[12] = new Ac4Extractor(); if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[12] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 440e577c7d8..5f1cf7ecb87 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -67,6 +67,8 @@ public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); + public static final int TYPE_ac_4 = Util.getIntegerCodeForString("ac-4"); + public static final int TYPE_dac4 = Util.getIntegerCodeForString("dac4"); public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index d085156f2bf..24b24d50117 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; +import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; @@ -684,6 +685,7 @@ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotat || childAtomType == Atom.TYPE_enca || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3 + || childAtomType == Atom.TYPE_ac_4 || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse || childAtomType == Atom.TYPE_dtsh @@ -974,6 +976,8 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType mimeType = MimeTypes.AUDIO_AC3; } else if (atomType == Atom.TYPE_ec_3) { mimeType = MimeTypes.AUDIO_E_AC3; + } else if (atomType == Atom.TYPE_ac_4) { + mimeType = MimeTypes.AUDIO_AC4; } else if (atomType == Atom.TYPE_dtsc) { mimeType = MimeTypes.AUDIO_DTS; } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) { @@ -1031,6 +1035,10 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType parent.setPosition(Atom.HEADER_SIZE + childPosition); out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language, drmInitData); + } else if (childAtomType == Atom.TYPE_dac4) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, + drmInitData); } else if (childAtomType == Atom.TYPE_ddts) { out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 72bcaa9fe28..1d458546471 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -1276,10 +1277,17 @@ private boolean readSample(ExtractorInput input) throws IOException, Interrupted } } } else { + int sampleHeaderSize = 0; + if (MimeTypes.AUDIO_AC4.equals(track.format.sampleMimeType)) { + ParsableByteArray ac4SampleHeaderData = Ac4Util.getAc4SampleHeader(sampleSize); + output.sampleData(ac4SampleHeaderData, ac4SampleHeaderData.capacity()); + sampleHeaderSize = ac4SampleHeaderData.capacity(); + } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; } + sampleSize += sampleHeaderSize; } @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index ec24bed9649..7c9fe2a1879 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -31,6 +32,7 @@ import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -536,11 +538,18 @@ private int readSample(ExtractorInput input, PositionHolder positionHolder) } } } else { + int sampleHeaderSize = 0; + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + ParsableByteArray ac4SampleHeaderData = Ac4Util.getAc4SampleHeader(sampleSize); + trackOutput.sampleData(ac4SampleHeaderData, ac4SampleHeaderData.capacity()); + sampleHeaderSize = ac4SampleHeaderData.capacity(); + } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } + sampleSize += sampleHeaderSize; } trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], track.sampleTable.flags[sampleIndex], sampleSize, 0, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java new file mode 100644 index 00000000000..8ecab956e68 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018 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.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.Ac4Util; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +import java.io.IOException; + +/** + * Extracts data from AC-4 bitstreams. + */ +public final class Ac4Extractor implements Extractor { + + /** + * Factory for {@link Ac4Extractor} instances. + */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()}; + + /** + * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving + * up. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + private static final int AC40_SYNC_WORD = 0xAC40; + private static final int AC41_SYNC_WORD = 0xAC41; + /** + * Typical AC-4 frame size (in bytes) + * The value 16 kB is determined by the maximum frame size used in broadcast applications. + */ + private static final int FRAMELEN_TYPICAL = 16384; /* in bytes (16 kB) */ + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + + private final long firstSampleTimestampUs; + private final Ac4Reader reader; + private final ParsableByteArray sampleData; + + private boolean startedPacket; + + public Ac4Extractor() { + this(0); + } + + public Ac4Extractor(long firstSampleTimestampUs) { + this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac4Reader(); + sampleData = new ParsableByteArray(FRAMELEN_TYPICAL); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(10); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, 0, 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + int headerPosition = startPosition; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, 0, 7); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC40_SYNC_WORD && syncBytes != AC41_SYNC_WORD) { + validFramesCount = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4) { + return true; + } + int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.data, syncBytes); + if (frameSize == C.LENGTH_UNSET) { + return false; + } + input.advancePeekPosition(frameSize - 7); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader.createTracks(output, new TrackIdGenerator(0, 1)); + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + + @Override + public void seek(long position, long timeUs) { + startedPacket = false; + reader.seek(); + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + int bytesRead = input.read(sampleData.data, 0, FRAMELEN_TYPICAL); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + sampleData.setPosition(0); + sampleData.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(firstSampleTimestampUs, true); + startedPacket = true; + } + // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(sampleData); + return RESULT_CONTINUE; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java new file mode 100644 index 00000000000..b0d4723edef --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2018 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.exoplayer2.extractor.ts; + +import android.support.annotation.IntDef; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.Ac4Util; +import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Parses a continuous AC-4 byte stream and extracts individual samples. + */ +public final class Ac4Reader implements ElementaryStreamReader { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} + + 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 final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + private final String language; + + private String trackFormatId; + private TrackOutput output; + + @State private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWasAC; + private boolean hasCRC; + + // Used when parsing the header. + private long sampleDurationUs; + private Format format; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for AC-4 elementary streams. + */ + public Ac4Reader() { + this(null); + } + + /** + * Constructs a new reader for AC-4 elementary streams. + * + * @param language Track language. + */ + public Ac4Reader(String language) { + headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.data); + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = (byte)0xAC; + headerScratchBytes.data[1] = 0x40; + if (hasCRC) + headerScratchBytes.data[1] = 0x41; + bytesRead = 2; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, Ac4Util.HEADER_SIZE_FOR_PARSER)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, Ac4Util.HEADER_SIZE_FOR_PARSER); + 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.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + default: + 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 syncword, advancing the position to the byte that immediately follows it. If a + * syncword was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return Whether a syncword position was found. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWasAC) { + lastByteWasAC = (pesBuffer.readUnsignedByte() == 0xAC); + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + if (secondByte == 0x40 || secondByte == 0x41) { + lastByteWasAC = false; + hasCRC = (secondByte == 0x41); + return true; + } else { + lastByteWasAC = (secondByte == 0xAC); + } + } + return false; + } + + /** + * Parses the sample header. + */ + @SuppressWarnings("ReferenceEquality") + private void parseHeader() { + headerScratchBits.setPosition(0); + SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); + if (format == null || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || frameInfo.mimeType != format.sampleMimeType) { + format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, + null, 0, language); + output.format(format); + } + sampleSize = frameInfo.frameSize; + // In this class a sample is an AC-4 sync frame, but the MediaFormat sample rate specifies the + // number of PCM audio samples per second. + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index a5506e2cfbe..488480d10e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -142,6 +142,8 @@ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AC4: + return new PesReader(new Ac4Reader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: return new PesReader(new DtsReader(esInfo.language)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f47a481d7e0..21dec0d1111 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -85,6 +85,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_DTS = 0x8A; public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; public static final int TS_STREAM_TYPE_E_AC3 = 0x87; + public static final int TS_STREAM_TYPE_AC4 = 0xAC; /* DVB/ATSC AC-4 Descriptor */ public static final int TS_STREAM_TYPE_H262 = 0x02; public static final int TS_STREAM_TYPE_H264 = 0x1B; public static final int TS_STREAM_TYPE_H265 = 0x24; @@ -100,6 +101,7 @@ public final class TsExtractor implements Extractor { private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); + private static final long AC4_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-4"); private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; @@ -484,8 +486,11 @@ private class PmtReader implements SectionPayloadReader { private static final int TS_PMT_DESC_AC3 = 0x6A; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVB_EXT = 0x7F; private static final int TS_PMT_DESC_DVBSUBS = 0x59; + private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15; + private final ParsableBitArray pmtScratch; private final SparseArray trackIdToReaderScratch; private final SparseIntArray trackIdToPidScratch; @@ -638,6 +643,8 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { streamType = TS_STREAM_TYPE_AC3; } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) { streamType = TS_STREAM_TYPE_E_AC3; + } else if (formatIdentifier == AC4_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_AC4; } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) { streamType = TS_STREAM_TYPE_H265; } @@ -645,6 +652,13 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { streamType = TS_STREAM_TYPE_AC3; } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor streamType = TS_STREAM_TYPE_E_AC3; + } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) { + // extension descriptor in DVB (ETSI EN 300 468) + int descriptorTagExt = data.readUnsignedByte(); + if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) { + // AC-4_descriptor in DVB (ETSI EN 300 468) + streamType = TS_STREAM_TYPE_AC4; + } } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor streamType = TS_STREAM_TYPE_DTS; } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { 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 e506ae1b19b..e6cf5ed95ee 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 @@ -55,6 +55,7 @@ public final class MimeTypes { public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_E_AC3_JOC = BASE_TYPE_AUDIO + "/eac3-joc"; + public static final String AUDIO_AC4 = BASE_TYPE_AUDIO + "/ac4"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -237,6 +238,8 @@ public static boolean isApplication(@Nullable String mimeType) { return MimeTypes.AUDIO_E_AC3; } else if (codec.startsWith("ec+3")) { return MimeTypes.AUDIO_E_AC3_JOC; + } else if (codec.startsWith("ac-4") || codec.startsWith("dac4")) { + return MimeTypes.AUDIO_AC4; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -301,6 +304,8 @@ public static String getMimeTypeFromMp4ObjectType(int objectType) { return MimeTypes.AUDIO_DTS_HD; case 0xAD: return MimeTypes.AUDIO_OPUS; + case 0xAE: + return MimeTypes.AUDIO_AC4; default: return null; } @@ -354,6 +359,8 @@ public static int getTrackType(@Nullable String mimeType) { case MimeTypes.AUDIO_E_AC3: case MimeTypes.AUDIO_E_AC3_JOC: return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_AC4: + return C.ENCODING_AC4; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; case MimeTypes.AUDIO_DTS_HD: diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 8a403c37592..b1e78c22f27 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.TsExtractor; @@ -43,6 +44,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String AAC_FILE_EXTENSION = ".aac"; public static final String AC3_FILE_EXTENSION = ".ac3"; + public static final String AC4_FILE_EXTENSION = ".ac4"; public static final String EC3_FILE_EXTENSION = ".ec3"; public static final String MP3_FILE_EXTENSION = ".mp3"; public static final String MP4_FILE_EXTENSION = ".mp4"; @@ -94,6 +96,8 @@ public Pair createExtractor( return buildResult(new AdtsExtractor()); } else if (previousExtractor instanceof Ac3Extractor) { return buildResult(new Ac3Extractor()); + } else if (previousExtractor instanceof Ac4Extractor) { + return buildResult(new Ac4Extractor()); } else if (previousExtractor instanceof Mp3Extractor) { return buildResult(new Mp3Extractor()); } else { @@ -135,6 +139,13 @@ public Pair createExtractor( } } + if (!(extractorByFileExtension instanceof Ac4Extractor)) { + Ac4Extractor ac4Extractor = new Ac4Extractor(); + if (sniffQuietly(ac4Extractor, extractorInput)) { + return buildResult(ac4Extractor); + } + } + if (!(extractorByFileExtension instanceof Mp3Extractor)) { Mp3Extractor mp3Extractor = new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); @@ -188,6 +199,8 @@ private Extractor createExtractorByFileExtension( } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { return new Ac3Extractor(); + } else if (lastPathSegment.endsWith(AC4_FILE_EXTENSION)) { + return new Ac4Extractor(); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) @@ -254,6 +267,7 @@ private static Pair buildResult(Extractor extractor) { extractor, extractor instanceof AdtsExtractor || extractor instanceof Ac3Extractor + || extractor instanceof Ac4Extractor || extractor instanceof Mp3Extractor); }