diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 63edfff07c..6158ea9e07 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,8 @@ * Fix an issue that caused audio to be truncated at the end of a period when switching to a new period where gapless playback information was newly present or newly absent. + * Add support for reading AC-4 streams + ([#5303](https://github.com/google/ExoPlayer/pull/5303)). * Add support for SHOUTcast ICY metadata ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * CEA-608: Improved conformance to the specification diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java index ef6f4e97bb..611197bbe5 100644 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; 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.PsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; @@ -59,6 +60,7 @@ public final class DefaultExtractorsFactoryTest { Mp3Extractor.class, AdtsExtractor.class, Ac3Extractor.class, + Ac4Extractor.class, TsExtractor.class, FlvExtractor.class, OggExtractor.class, 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 d0676e946d..04a90b38d8 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 @@ -146,8 +146,8 @@ public final class 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_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or + * {@link #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -163,9 +163,10 @@ public final class C { ENCODING_PCM_A_LAW, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD + ENCODING_DOLBY_TRUEHD, }) public @interface Encoding {} @@ -209,6 +210,8 @@ public final class C { public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; + /** @see AudioFormat#ENCODING_AC4 */ + public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; /** @see AudioFormat#ENCODING_DTS */ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; /** @see AudioFormat#ENCODING_DTS_HD */ 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 0000000000..74bd5bfe98 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2019 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 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( + int bitstreamVersion, int channelCount, int sampleRate, int frameSize, int sampleCount) { + this.bitstreamVersion = bitstreamVersion; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.frameSize = frameSize; + this.sampleCount = sampleCount; + } + } + + public static final int AC40_SYNCWORD = 0xAC40; + public static final int AC41_SYNCWORD = 0xAC41; + + /** The channel count of AC-4 stream. */ + // TODO: Parse AC-4 stream channel count. + private 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 48 kHz. For 44.1 + * kHz, 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); // ac4_dsi_version, bitstream_version[0:5] + int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; + return Format.createAudioSampleFormat( + trackId, + MimeTypes.AUDIO_AC4, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + CHANNEL_COUNT_2, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); + } + + /** + * 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; // Extended frame_size + } + frameSize += headerSize; + if (syncWord == AC41_SYNCWORD) { + frameSize += 2; // crc_word + } + int bitstreamVersion = data.readBits(2); + if (bitstreamVersion == 3) { + bitstreamVersion += readVariableBits(data, /* bitsPerRead= */ 2); + } + int sequenceCounter = data.readBits(10); + if (data.readBit()) { // b_wait_frames + if (data.readBits(3) > 0) { // wait_frames + data.skipBits(2); // reserved + } + } + int sampleRate = data.readBit() ? 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: // fall through + 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(bitstreamVersion, CHANNEL_COUNT_2, sampleRate, frameSize, sampleCount); + } + + /** + * Returns the size in bytes of the given AC-4 syncframe. + * + * @param data The syncframe to parse. + * @param syncword The syncword value for the syncframe. + * @return The syncframe size in bytes, or {@link C#LENGTH_UNSET} if the input is invalid. + */ + public static int parseAc4SyncframeSize(byte[] data, int syncword) { + 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 (syncword == AC41_SYNCWORD) { + 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]; + int position = buffer.position(); + buffer.get(bufferBytes); + buffer.position(position); + return parseAc4SyncframeInfo(new ParsableBitArray(bufferBytes)).sampleCount; + } + + /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ + public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { + // See ETSI TS 103 190-1 V1.3.1, Annex G. + buffer.reset(/* limit= */ 7); + buffer.data[0] = (byte) 0xAC; + buffer.data[1] = 0x40; + buffer.data[2] = (byte) 0xFF; + buffer.data[3] = (byte) 0xFF; + buffer.data[4] = (byte) ((size >> 16) & 0xFF); + buffer.data[5] = (byte) ((size >> 8) & 0xFF); + buffer.data[6] = (byte) (size & 0xFF); + } + + private static int readVariableBits(ParsableBitArray data, int bitsPerRead) { + int value = 0; + while (true) { + value += data.readBits(bitsPerRead); + if (!data.readBit()) { + break; + } + value++; + value <<= bitsPerRead; + } + return value; + } + + 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 f1c64e904c..ffcd893e7b 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 @@ -1126,6 +1126,8 @@ public final class DefaultAudioSink implements AudioSink { return 640 * 1000 / 8; case C.ENCODING_E_AC3: return 6144 * 1000 / 8; + case C.ENCODING_AC4: + return 2688 * 1000 / 8; case C.ENCODING_DTS: // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. return 1536 * 1000 / 8; @@ -1154,6 +1156,8 @@ 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_AC4) { + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); return syncframeOffset == C.INDEX_UNSET 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 41f16c0e52..3d46496eed 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 @@ -784,6 +784,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } + if (Util.SDK_INT <= 28 && MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + // On some older builds, the AC-4 decoder expects to receive samples formatted as raw frames + // not sync frames. Set a format key to override this. + 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 54bb617c58..54c78eb33d 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.FragmentedMp4Extractor; 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; @@ -47,6 +48,7 @@ import java.lang.reflect.Constructor; *
  • FLV ({@link FlvExtractor}) *
  • WAV ({@link WavExtractor}) *
  • AC3 ({@link Ac3Extractor}) + *
  • AC4 ({@link Ac4Extractor}) *
  • AMR ({@link AmrExtractor}) *
  • FLAC (only available if the FLAC extension is built and included) * @@ -206,7 +208,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @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 +237,10 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { | (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 f383305fcc..0bc5f18eef 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 @@ -80,6 +80,8 @@ import java.util.List; 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 0a87ab92c4..0185a6d8af 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 @@ -23,6 +23,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.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; @@ -756,6 +757,7 @@ import java.util.List; || 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 @@ -1071,6 +1073,8 @@ import java.util.List; 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) { @@ -1128,6 +1132,10 @@ import java.util.List; 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 efecbb60ac..4f45e85762 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 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.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -133,13 +134,14 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalStartCode; private final ParsableByteArray nalPrefix; private final ParsableByteArray nalBuffer; + private final byte[] scratchBytes; + private final ParsableByteArray scratch; // Adjusts sample timestamps. private final @Nullable TimestampAdjuster timestampAdjuster; // Parser state. private final ParsableByteArray atomHeader; - private final byte[] extendedTypeScratch; private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; private final @Nullable TrackOutput additionalEmsgTrackOutput; @@ -160,6 +162,7 @@ public class FragmentedMp4Extractor implements Extractor { private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; + private boolean isAc4HeaderRequired; // Extractor output. private ExtractorOutput extractorOutput; @@ -254,7 +257,8 @@ public class FragmentedMp4Extractor implements Extractor { nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); - extendedTypeScratch = new byte[16]; + scratchBytes = new byte[16]; + scratch = new ParsableByteArray(scratchBytes); containerAtoms = new ArrayDeque<>(); pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); @@ -291,6 +295,7 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); + isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -538,7 +543,7 @@ public class FragmentedMp4Extractor implements Extractor { } private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { - parseMoof(moof, trackBundles, flags, extendedTypeScratch); + parseMoof(moof, trackBundles, flags, scratchBytes); // If drm init data is sideloaded, we ignore pssh boxes. DrmInitData drmInitData = sideloadedDrmInitData != null ? null : getDrmInitDataFromAtoms(moof.leafChildren); @@ -1224,6 +1229,8 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; + isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; @@ -1288,6 +1295,14 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + int length = scratch.limit(); + output.sampleData(scratch, length); + sampleSize += length; + sampleBytesWritten += length; + isAc4HeaderRequired = false; + } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; 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 2a61e7dd15..60613cf818 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 androidx.annotation.IntDef; 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.TrackOutput; 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; @@ -95,6 +97,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; @@ -108,6 +111,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int sampleTrackIndex; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; + private boolean isAc4HeaderRequired; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -136,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms = new ArrayDeque<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); + scratch = new ParsableByteArray(); sampleTrackIndex = C.INDEX_UNSET; } @@ -156,6 +161,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; + isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -493,6 +499,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } + isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; @@ -546,6 +554,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } } else { + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + int length = scratch.limit(); + trackOutput.sampleData(scratch, length); + sampleSize += length; + sampleBytesWritten += length; + isAc4HeaderRequired = false; + } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 3741d52294..889a49755a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -76,7 +76,7 @@ public final class Ac3Extractor implements Extractor { if (scratch.readUnsignedInt24() != ID3_TAG) { break; } - scratch.skipBytes(3); + scratch.skipBytes(3); // version, flags int length = scratch.readSynchSafeInt(); startPosition += 10 + length; input.advancePeekPosition(length); 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 0000000000..133c0f368b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2019 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 static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; +import static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + +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; + + /** + * The size of the reading buffer, in bytes. This value is determined based on the maximum frame + * size used in broadcast applications. + */ + private static final int READ_BUFFER_SIZE = 16384; + + /** The size of the frame header, in bytes. */ + private static final int FRAME_HEADER_SIZE = 7; + + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + + private final long firstSampleTimestampUs; + private final Ac4Reader reader; + private final ParsableByteArray sampleData; + + private boolean startedPacket; + + /** Creates a new extractor for AC-4 bitstreams. */ + public Ac4Extractor() { + this(/* firstSampleTimestampUs= */ 0); + } + + /** Creates a new extractor for AC-4 bitstreams, using the specified first sample timestamp. */ + public Ac4Extractor(long firstSampleTimestampUs) { + this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac4Reader(); + sampleData = new ParsableByteArray(READ_BUFFER_SIZE); + } + + // 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, /* offset= */ 0, /* length= */ 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); // version, flags + 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, /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) { + 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 - FRAME_HEADER_SIZE); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader.createTracks( + output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1)); + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(/* durationUs= */ 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, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE); + 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, FLAG_DATA_ALIGNMENT_INDICATOR); + 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 0000000000..48bd07fce4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2019 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 androidx.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.MimeTypes; +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, @TsPayloadReader.Flags int flags) { + 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] = (byte) (hasCRC ? 0x41 : 0x40); + 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(); + lastByteWasAC = secondByte == 0xAC; + if (secondByte == 0x40 || secondByte == 0x41) { + hasCRC = secondByte == 0x41; + return true; + } + } + 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 + || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + trackFormatId, + MimeTypes.AUDIO_AC4, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); + output.format(format); + } + sampleSize = frameInfo.frameSize; + // In this class a sample is an AC-4 sync frame, but Format#sampleRate 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 ed708f349e..4ddac6f761 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 @@ -149,6 +149,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact 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_HDMV_DTS: if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) { return null; 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 c243458dd3..a2f8568cbb 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 @@ -87,6 +87,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; @@ -102,6 +103,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; @@ -494,8 +496,11 @@ public final class TsExtractor implements Extractor { 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; @@ -648,6 +653,8 @@ public final class TsExtractor implements Extractor { 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; } @@ -655,6 +662,13 @@ public final class TsExtractor implements Extractor { 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 5eaed8ad2f..e603f76dbc 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 @@ -58,6 +58,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"; @@ -228,6 +229,8 @@ public final class MimeTypes { 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")) { @@ -292,6 +295,8 @@ public final class MimeTypes { return MimeTypes.AUDIO_DTS_HD; case 0xAD: return MimeTypes.AUDIO_OPUS; + case 0xAE: + return MimeTypes.AUDIO_AC4; default: return null; } @@ -345,6 +350,8 @@ public final class MimeTypes { 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/core/src/test/assets/ts/sample.ac4 b/library/core/src/test/assets/ts/sample.ac4 new file mode 100644 index 0000000000..721f53cdd7 Binary files /dev/null and b/library/core/src/test/assets/ts/sample.ac4 differ diff --git a/library/core/src/test/assets/ts/sample.ac4.0.dump b/library/core/src/test/assets/ts/sample.ac4.0.dump new file mode 100644 index 0000000000..03ae07707a --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ac4.0.dump @@ -0,0 +1,106 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 7594 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 366, hash B4277F9E + sample 1: + time = 40000 + flags = 1 + data = length 366, hash E8E0A142 + sample 2: + time = 80000 + flags = 1 + data = length 366, hash 2E5073D0 + sample 3: + time = 120000 + flags = 1 + data = length 366, hash 850E71D8 + sample 4: + time = 160000 + flags = 1 + data = length 366, hash 69CD444E + sample 5: + time = 200000 + flags = 1 + data = length 366, hash BD24F36D + sample 6: + time = 240000 + flags = 1 + data = length 366, hash E24F2490 + sample 7: + time = 280000 + flags = 1 + data = length 366, hash EE6F1F06 + sample 8: + time = 320000 + flags = 1 + data = length 366, hash 2DAB000F + sample 9: + time = 360000 + flags = 1 + data = length 366, hash 8102B7EC + sample 10: + time = 400000 + flags = 1 + data = length 366, hash 55BF59AC + sample 11: + time = 440000 + flags = 1 + data = length 494, hash CBC2E09F + sample 12: + time = 480000 + flags = 1 + data = length 519, hash 9DAF56E9 + sample 13: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 14: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 15: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 16: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 17: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 18: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index 8de7441496..be9100cb9d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; 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.PsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; @@ -62,7 +63,8 @@ public final class DefaultExtractorsFactoryTest { OggExtractor.class, PsExtractor.class, WavExtractor.class, - AmrExtractor.class + AmrExtractor.class, + Ac4Extractor.class }; assertThat(listCreatedExtractorClasses).containsNoDuplicates(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java new file mode 100644 index 0000000000..3d1bafc7dc --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link Ac4Extractor}. */ +@RunWith(AndroidJUnit4.class) +public final class Ac4ExtractorTest { + + @Test + public void testAc4Sample() throws Exception { + ExtractorAsserts.assertBehavior(Ac4Extractor::new, "ts/sample.ac4"); + } +} 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 8be0371072..8de2e425f8 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; 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 EC3_FILE_EXTENSION = ".ec3"; + public static final String AC4_FILE_EXTENSION = ".ac4"; public static final String MP3_FILE_EXTENSION = ".mp3"; public static final String MP4_FILE_EXTENSION = ".mp4"; public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; @@ -128,6 +130,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } } + 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); @@ -181,6 +190,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } 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) @@ -250,6 +261,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { 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 { @@ -262,6 +275,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { extractor, extractor instanceof AdtsExtractor || extractor instanceof Ac3Extractor + || extractor instanceof Ac4Extractor || extractor instanceof Mp3Extractor, isReusable(extractor)); }