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