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));
}