diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
index 8d33f95640..a4349ada09 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -112,16 +112,11 @@ public final class Mp3Extractor implements Extractor {
private long samplesRead;
private int sampleBytesRemaining;
- /**
- * Constructs a new {@link Mp3Extractor}.
- */
public Mp3Extractor() {
this(0);
}
/**
- * Constructs a new {@link Mp3Extractor}.
- *
* @param flags Flags that control the extractor's behavior.
*/
public Mp3Extractor(@Flags int flags) {
@@ -129,8 +124,6 @@ public final class Mp3Extractor implements Extractor {
}
/**
- * Constructs a new {@link Mp3Extractor}.
- *
* @param flags Flags that control the extractor's behavior.
* @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
* {@link C#TIME_UNSET} if forcing is not required.
@@ -144,6 +137,8 @@ public final class Mp3Extractor implements Extractor {
basisTimeUs = C.TIME_UNSET;
}
+ // Extractor implementation.
+
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
return synchronize(input, true);
@@ -195,6 +190,8 @@ public final class Mp3Extractor implements Extractor {
return readSample(input);
}
+ // Internal methods.
+
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (sampleBytesRemaining == 0) {
extractorInput.resetPeekPosition();
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 8bab6b7ed1..4d54600c6d 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
@@ -29,8 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
- * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3
- * bitstreams.
+ * Extracts samples from (E-)AC-3 bitstreams.
*/
public final class Ac3Extractor implements Extractor {
@@ -71,6 +70,8 @@ public final class Ac3Extractor implements Extractor {
sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);
}
+ // Extractor implementation.
+
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
// Skip any ID3 headers.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
index a1851aa0ea..5ce15952a5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -29,8 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
- * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
- * headers.
+ * Extracts samples from AAC bit streams with ADTS framing.
*/
public final class AdtsExtractor implements Extractor {
@@ -70,6 +69,8 @@ public final class AdtsExtractor implements Extractor {
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
}
+ // Extractor implementation.
+
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
// Skip any ID3 headers.
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 bd013f96a3..2d16b46895 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
@@ -28,7 +28,7 @@ import java.util.Collections;
import java.util.List;
/**
- * Default implementation for {@link TsPayloadReader.Factory}.
+ * Default {@link TsPayloadReader.Factory} implementation.
*/
public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {
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
new file mode 100644
index 0000000000..9f0989e444
--- /dev/null
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 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.source.hls;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.extractor.Extractor;
+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.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Default {@link HlsExtractorFactory} implementation.
+ *
+ *
This class can be extended to override {@link TsExtractor} instantiation.
+ */
+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 MP3_FILE_EXTENSION = ".mp3";
+ public static final String MP4_FILE_EXTENSION = ".mp4";
+ public static final String M4_FILE_EXTENSION_PREFIX = ".m4";
+ public static final String VTT_FILE_EXTENSION = ".vtt";
+ public static final String WEBVTT_FILE_EXTENSION = ".webvtt";
+
+ @Override
+ public Pair createExtractor(Extractor previousExtractor, Uri uri,
+ Format format, List muxedCaptionFormats, DrmInitData drmInitData,
+ TimestampAdjuster timestampAdjuster) {
+ String lastPathSegment = uri.getLastPathSegment();
+ boolean isPackedAudioExtractor = false;
+ Extractor extractor;
+ if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)
+ || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
+ || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
+ extractor = new WebvttExtractor(format.language, timestampAdjuster);
+ } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
+ isPackedAudioExtractor = true;
+ extractor = new AdtsExtractor();
+ } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+ || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
+ isPackedAudioExtractor = true;
+ extractor = new Ac3Extractor();
+ } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
+ isPackedAudioExtractor = true;
+ extractor = new Mp3Extractor(0, 0);
+ } else if (previousExtractor != null) {
+ // Only reuse TS and fMP4 extractors.
+ extractor = previousExtractor;
+ } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
+ || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) {
+ extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData);
+ } else {
+ // For any other file extension, we assume TS format.
+ @DefaultTsPayloadReaderFactory.Flags
+ int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
+ if (muxedCaptionFormats != null) {
+ // The playlist declares closed caption renditions, we should ignore descriptors.
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
+ } else {
+ muxedCaptionFormats = Collections.emptyList();
+ }
+ String codecs = format.codecs;
+ if (!TextUtils.isEmpty(codecs)) {
+ // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
+ // exist. If we know from the codec attribute that they don't exist, then we can
+ // explicitly ignore them even if they're declared.
+ if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
+ }
+ if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
+ }
+ }
+ extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
+ new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats));
+ }
+ return Pair.create(extractor, isPackedAudioExtractor);
+ }
+
+}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
index 8aa4e057a2..b8a0c3ddb7 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -80,6 +80,7 @@ import java.util.List;
}
+ private final HlsExtractorFactory extractorFactory;
private final DataSource mediaDataSource;
private final DataSource encryptionDataSource;
private final TimestampAdjusterProvider timestampAdjusterProvider;
@@ -106,6 +107,8 @@ import java.util.List;
private long liveEdgeTimeUs;
/**
+ * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for
+ * media chunks.
* @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists.
* @param variants The available variants.
* @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the
@@ -116,9 +119,10 @@ import java.util.List;
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist.
*/
- public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants,
- HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider,
- List muxedCaptionFormats) {
+ public HlsChunkSource(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker,
+ HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory,
+ TimestampAdjusterProvider timestampAdjusterProvider, List muxedCaptionFormats) {
+ this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker;
this.variants = variants;
this.timestampAdjusterProvider = timestampAdjusterProvider;
@@ -321,11 +325,11 @@ import java.util.List;
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
- out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl,
- muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
- startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
- isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey,
- encryptionIv);
+ out.chunk = new HlsMediaChunk(extractorFactory, mediaDataSource, dataSpec, initDataSpec,
+ selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(),
+ trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs,
+ chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous,
+ mediaPlaylist.drmInitData, encryptionKey, encryptionIv);
}
/**
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java
new file mode 100644
index 0000000000..3ed6a549db
--- /dev/null
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 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.source.hls;
+
+import android.net.Uri;
+import android.util.Pair;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.List;
+
+/**
+ * Factory for HLS media chunk extractors.
+ */
+public interface HlsExtractorFactory {
+
+ HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory();
+
+ /**
+ * Creates an {@link Extractor} for extracting HLS media chunks.
+ *
+ * @param previousExtractor A previously used {@link Extractor} which can be reused if the current
+ * chunk is a continuation of the previously extracted chunk, or null otherwise. It is the
+ * responsibility of implementers to only reuse extractors that are suited for reusage.
+ * @param uri The URI of the media chunk.
+ * @param format A {@link Format} associated with the chunk to extract.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
+ * information is available in the master playlist.
+ * @param drmInitData {@link DrmInitData} associated with the chunk.
+ * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
+ * @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a
+ * packed audio extractor. The first element may be {@code previousExtractor} if the factory
+ * has determined it can be re-used.
+ */
+ Pair createExtractor(Extractor previousExtractor, Uri uri, Format format,
+ List muxedCaptionFormats, DrmInitData drmInitData,
+ TimestampAdjuster timestampAdjuster);
+
+}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
index 91513b536e..5ca8675dd9 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -15,19 +15,13 @@
*/
package com.google.android.exoplayer2.source.hls;
-import android.text.TextUtils;
+import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
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.AdtsExtractor;
-import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
-import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
@@ -35,12 +29,10 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -49,19 +41,11 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
/* package */ final class HlsMediaChunk extends MediaChunk {
- private static final AtomicInteger UID_SOURCE = new AtomicInteger();
private static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp";
- private static final String AAC_FILE_EXTENSION = ".aac";
- private static final String AC3_FILE_EXTENSION = ".ac3";
- private static final String EC3_FILE_EXTENSION = ".ec3";
- private static final String MP3_FILE_EXTENSION = ".mp3";
- private static final String MP4_FILE_EXTENSION = ".mp4";
- private static final String M4_FILE_EXTENSION_PREFIX = ".m4";
- private static final String VTT_FILE_EXTENSION = ".vtt";
- private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
+ private static final AtomicInteger uidSource = new AtomicInteger();
/**
* A unique identifier for the chunk.
@@ -83,26 +67,24 @@ import java.util.concurrent.atomic.AtomicInteger;
private final boolean isEncrypted;
private final boolean isMasterTimestampSource;
private final TimestampAdjuster timestampAdjuster;
- private final String lastPathSegment;
- private final Extractor previousExtractor;
private final boolean shouldSpliceIn;
- private final boolean needNewExtractor;
- private final List muxedCaptionFormats;
- private final DrmInitData drmInitData;
-
- private final boolean isPackedAudio;
+ private final Extractor extractor;
+ private final boolean isPackedAudioExtractor;
+ private final boolean reusingExtractor;
private final Id3Decoder id3Decoder;
private final ParsableByteArray id3Data;
- private Extractor extractor;
+ private HlsSampleStreamWrapper output;
private int initSegmentBytesLoaded;
private int bytesLoaded;
+ private boolean id3TimestampPeeked;
private boolean initLoadCompleted;
- private HlsSampleStreamWrapper extractorOutput;
private volatile boolean loadCanceled;
private volatile boolean loadCompleted;
/**
+ * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk
+ * extractor is obtained.
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param initDataSpec Defines the initialization data to be fed to new extractors. May be null.
@@ -124,10 +106,10 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param encryptionIv The AES initialization vector, or null if the segment is not fully
* encrypted.
*/
- public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
- HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason,
- Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex,
- int discontinuitySequenceNumber, boolean isMasterTimestampSource,
+ public HlsMediaChunk(HlsExtractorFactory extractorFactory, DataSource dataSource,
+ DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List muxedCaptionFormats,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+ int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource,
TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData,
byte[] fullSegmentEncryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec,
@@ -136,33 +118,34 @@ import java.util.concurrent.atomic.AtomicInteger;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec;
this.hlsUrl = hlsUrl;
- this.muxedCaptionFormats = muxedCaptionFormats;
this.isMasterTimestampSource = isMasterTimestampSource;
this.timestampAdjuster = timestampAdjuster;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
- this.drmInitData = drmInitData;
- lastPathSegment = dataSpec.uri.getLastPathSegment();
- isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
- || lastPathSegment.endsWith(AC3_FILE_EXTENSION)
- || lastPathSegment.endsWith(EC3_FILE_EXTENSION)
- || lastPathSegment.endsWith(MP3_FILE_EXTENSION);
+ Extractor previousExtractor = null;
if (previousChunk != null) {
- id3Decoder = previousChunk.id3Decoder;
- id3Data = previousChunk.id3Data;
- previousExtractor = previousChunk.extractor;
shouldSpliceIn = previousChunk.hlsUrl != hlsUrl;
- needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
- || shouldSpliceIn;
+ previousExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
+ || shouldSpliceIn ? null : previousChunk.extractor;
} else {
- id3Decoder = isPackedAudio ? new Id3Decoder() : null;
- id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null;
- previousExtractor = null;
shouldSpliceIn = false;
- needNewExtractor = true;
+ }
+ Pair extractorData = extractorFactory.createExtractor(previousExtractor,
+ dataSpec.uri, trackFormat, muxedCaptionFormats, drmInitData, timestampAdjuster);
+ extractor = extractorData.first;
+ isPackedAudioExtractor = extractorData.second;
+ reusingExtractor = extractor == previousExtractor;
+ initLoadCompleted = reusingExtractor && initDataSpec != null;
+ if (isPackedAudioExtractor) {
+ id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder();
+ id3Data = previousChunk != null ? previousChunk.id3Data
+ : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
+ } else {
+ id3Decoder = null;
+ id3Data = null;
}
initDataSource = dataSource;
- uid = UID_SOURCE.getAndIncrement();
+ uid = uidSource.getAndIncrement();
}
/**
@@ -172,8 +155,11 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param output The output that will receive the loaded samples.
*/
public void init(HlsSampleStreamWrapper output) {
- extractorOutput = output;
+ this.output = output;
output.init(uid, shouldSpliceIn);
+ if (!reusingExtractor) {
+ extractor.init(output);
+ }
}
@Override
@@ -200,10 +186,6 @@ import java.util.concurrent.atomic.AtomicInteger;
@Override
public void load() throws IOException, InterruptedException {
- if (extractor == null && !isPackedAudio) {
- // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction.
- extractor = createExtractor();
- }
maybeLoadInitData();
if (!loadCanceled) {
loadMedia();
@@ -213,8 +195,8 @@ import java.util.concurrent.atomic.AtomicInteger;
// Internal loading methods.
private void maybeLoadInitData() throws IOException, InterruptedException {
- if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) {
- // According to spec, for packed audio, initDataSpec is expected to be null.
+ if (initLoadCompleted || initDataSpec == null) {
+ // Note: The HLS spec forbids initialization segments for packed audio.
return;
}
DataSpec initSegmentDataSpec = initDataSpec.subrange(initSegmentBytesLoaded);
@@ -258,10 +240,10 @@ import java.util.concurrent.atomic.AtomicInteger;
try {
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
- if (extractor == null) {
- // Media segment format is packed audio.
+ if (isPackedAudioExtractor && !id3TimestampPeeked) {
long id3Timestamp = peekId3PrivTimestamp(input);
- extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET
+ id3TimestampPeeked = true;
+ output.setSampleOffsetUs(id3Timestamp != C.TIME_UNSET
? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs);
}
if (skipLoadedBytes) {
@@ -345,68 +327,4 @@ import java.util.concurrent.atomic.AtomicInteger;
return dataSource;
}
- private Extractor createExtractor() {
- // Select the extractor that will read the chunk.
- Extractor extractor;
- boolean usingNewExtractor = true;
- if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType)
- || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
- || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
- extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster);
- } else if (!needNewExtractor) {
- // Only reuse TS and fMP4 extractors.
- usingNewExtractor = false;
- extractor = previousExtractor;
- } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
- || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) {
- extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData);
- } else {
- // MPEG-2 TS segments, but we need a new extractor.
- // This flag ensures the change of pid between streams does not affect the sample queues.
- @DefaultTsPayloadReaderFactory.Flags
- int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
- List closedCaptionFormats = muxedCaptionFormats;
- if (closedCaptionFormats != null) {
- // The playlist declares closed caption renditions, we should ignore descriptors.
- esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
- } else {
- closedCaptionFormats = Collections.emptyList();
- }
- String codecs = trackFormat.codecs;
- if (!TextUtils.isEmpty(codecs)) {
- // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
- // exist. If we know from the codec attribute that they don't exist, then we can
- // explicitly ignore them even if they're declared.
- if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
- esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
- }
- if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
- esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
- }
- }
- extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
- new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats));
- }
- if (usingNewExtractor) {
- extractor.init(extractorOutput);
- }
- return extractor;
- }
-
- private Extractor buildPackedAudioExtractor(long startTimeUs) {
- Extractor extractor;
- if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
- extractor = new AdtsExtractor(startTimeUs);
- } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
- || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
- extractor = new Ac3Extractor(startTimeUs);
- } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
- extractor = new Mp3Extractor(0, startTimeUs);
- } else {
- throw new IllegalArgumentException("Unknown extension for audio file: " + lastPathSegment);
- }
- extractor.init(extractorOutput);
- return extractor;
- }
-
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
index 003b38efef..ea9e52e62e 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -44,6 +44,7 @@ import java.util.List;
public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
HlsPlaylistTracker.PlaylistEventListener {
+ private final HlsExtractorFactory extractorFactory;
private final HlsPlaylistTracker playlistTracker;
private final HlsDataSourceFactory dataSourceFactory;
private final int minLoadableRetryCount;
@@ -60,8 +61,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
private CompositeSequenceableLoader sequenceableLoader;
- public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory,
- int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator) {
+ public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker,
+ HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount,
+ EventDispatcher eventDispatcher, Allocator allocator) {
+ this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
@@ -344,8 +347,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) {
- HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants,
- dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats);
+ HlsChunkSource defaultChunkSource = new HlsChunkSource(extractorFactory, playlistTracker,
+ variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats);
return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs,
muxedAudioFormat, minLoadableRetryCount, eventDispatcher);
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
index 10a0536612..f7f26bb37c 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -20,6 +20,7 @@ import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -51,6 +52,7 @@ public final class HlsMediaSource implements MediaSource,
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+ private final HlsExtractorFactory extractorFactory;
private final Uri manifestUri;
private final HlsDataSourceFactory dataSourceFactory;
private final int minLoadableRetryCount;
@@ -60,32 +62,57 @@ public final class HlsMediaSource implements MediaSource,
private HlsPlaylistTracker playlistTracker;
private Listener sourceListener;
+ /**
+ * @param manifestUri The {@link Uri} of the HLS manifest.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
+ * segments and keys.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of
+ * events is not required.
+ */
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener) {
this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler,
eventListener);
}
+ /**
+ * @param manifestUri The {@link Uri} of the HLS manifest.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
+ * segments and keys.
+ * @param minLoadableRetryCount The minimum number of times loads must be retried before
+ * errors are propagated.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of
+ * events is not required.
+ */
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
int minLoadableRetryCount, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener) {
- this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount,
- eventHandler, eventListener);
+ this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory),
+ HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener,
+ new HlsPlaylistParser());
}
+ /**
+ * @param manifestUri The {@link Uri} of the HLS manifest.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
+ * segments and keys.
+ * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments.
+ * @param minLoadableRetryCount The minimum number of times loads must be retried before
+ * errors are propagated.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of
+ * events is not required.
+ * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
+ */
public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory,
- int minLoadableRetryCount, Handler eventHandler,
- AdaptiveMediaSourceEventListener eventListener) {
- this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener,
- new HlsPlaylistParser());
- }
-
- public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory,
- int minLoadableRetryCount, Handler eventHandler,
+ HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener,
- ParsingLoadable.Parser playlistParser) {
+ ParsingLoadable.Parser playlistParser) {
this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory;
+ this.extractorFactory = extractorFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistParser = playlistParser;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
@@ -108,8 +135,8 @@ public final class HlsMediaSource implements MediaSource,
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0);
- return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount,
- eventDispatcher, allocator);
+ return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory,
+ minLoadableRetryCount, eventDispatcher, allocator);
}
@Override
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index b844988588..946ae24d17 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -103,6 +103,7 @@ import java.util.LinkedList;
private boolean[] trackGroupEnabledStates;
private boolean[] trackGroupIsAudioVideoFlags;
+ private long sampleOffsetUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private boolean pendingResetUpstreamFormats;
@@ -369,16 +370,16 @@ import java.util.LinkedList;
// SampleStream implementation.
- /* package */ boolean isReady(int trackGroupIndex) {
+ public boolean isReady(int trackGroupIndex) {
return loadingFinished || (!isPendingReset() && sampleQueues[trackGroupIndex].hasNextSample());
}
- /* package */ void maybeThrowError() throws IOException {
+ public void maybeThrowError() throws IOException {
loader.maybeThrowError();
chunkSource.maybeThrowError();
}
- /* package */ int readData(int trackGroupIndex, FormatHolder formatHolder,
+ public int readData(int trackGroupIndex, FormatHolder formatHolder,
DecoderInputBuffer buffer, boolean requireFormat) {
if (isPendingReset()) {
return C.RESULT_NOTHING_READ;
@@ -402,7 +403,7 @@ import java.util.LinkedList;
lastSeekPositionUs);
}
- /* package */ int skipData(int trackGroupIndex, long positionUs) {
+ public int skipData(int trackGroupIndex, long positionUs) {
SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
return sampleQueue.advanceToEnd();
@@ -573,6 +574,7 @@ import java.util.LinkedList;
}
}
SampleQueue trackOutput = new SampleQueue(allocator);
+ trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
sampleQueueTrackIds[trackCount] = id;
@@ -599,6 +601,15 @@ import java.util.LinkedList;
handler.post(maybeFinishPrepareRunnable);
}
+ // Called by the loading thread.
+
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
+ }
+ }
+
// Internal methods.
private void maybeFinishPrepare() {
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
index da73aa3996..355a8575ca 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -134,8 +134,9 @@ public final class HlsPlaylistTracker implements Loader.Callback