diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java new file mode 100644 index 0000000000..6f39e1bff8 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -0,0 +1,338 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import android.net.Uri; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.EOFException; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Default {@link HlsExtractorFactory} implementation. + */ +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"; + public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; + public static final String CMF_FILE_EXTENSION_PREFIX = ".cmf"; + public static final String VTT_FILE_EXTENSION = ".vtt"; + public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + + @DefaultTsPayloadReaderFactory.Flags private final int payloadReaderFactoryFlags; + private final boolean exposeCea608WhenMissingDeclarations; + + /** + * Equivalent to {@link #DefaultHlsExtractorFactory(int, boolean) new + * DefaultHlsExtractorFactory(payloadReaderFactoryFlags = 0, exposeCea608WhenMissingDeclarations = + * true)} + */ + public DefaultHlsExtractorFactory() { + this(/* payloadReaderFactoryFlags= */ 0, /* exposeCea608WhenMissingDeclarations */ true); + } + + /** + * Creates a factory for HLS segment extractors. + * + * @param payloadReaderFactoryFlags Flags to add when constructing any {@link + * DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code + * payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}. + * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should + * expose a CEA-608 track should the master playlist contain no Closed Captions declarations. + * If the master playlist contains any Closed Captions declarations, this flag is ignored. + */ + public DefaultHlsExtractorFactory( + int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) { + this.payloadReaderFactoryFlags = payloadReaderFactoryFlags; + this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations; + } + + @Override + public Result createExtractor( + @Nullable Extractor previousExtractor, + Uri uri, + Format format, + @Nullable List<Format> muxedCaptionFormats, + TimestampAdjuster timestampAdjuster, + Map<String, List<String>> responseHeaders, + ExtractorInput extractorInput) + throws InterruptedException, IOException { + + if (previousExtractor != null) { + // A extractor has already been successfully used. Return one of the same type. + if (isReusable(previousExtractor)) { + return buildResult(previousExtractor); + } else { + Result result = + buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster); + if (result == null) { + throw new IllegalArgumentException( + "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName()); + } + } + } + + // Try selecting the extractor by the file extension. + Extractor extractorByFileExtension = + createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); + extractorInput.resetPeekPosition(); + if (sniffQuietly(extractorByFileExtension, extractorInput)) { + return buildResult(extractorByFileExtension); + } + + // We need to manually sniff each known type, without retrying the one selected by file + // extension. + + if (!(extractorByFileExtension instanceof WebvttExtractor)) { + WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster); + if (sniffQuietly(webvttExtractor, extractorInput)) { + return buildResult(webvttExtractor); + } + } + + if (!(extractorByFileExtension instanceof AdtsExtractor)) { + AdtsExtractor adtsExtractor = new AdtsExtractor(); + if (sniffQuietly(adtsExtractor, extractorInput)) { + return buildResult(adtsExtractor); + } + } + + if (!(extractorByFileExtension instanceof Ac3Extractor)) { + Ac3Extractor ac3Extractor = new Ac3Extractor(); + if (sniffQuietly(ac3Extractor, extractorInput)) { + return buildResult(ac3Extractor); + } + } + + 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); + if (sniffQuietly(mp3Extractor, extractorInput)) { + return buildResult(mp3Extractor); + } + } + + if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { + FragmentedMp4Extractor fragmentedMp4Extractor = + createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { + return buildResult(fragmentedMp4Extractor); + } + } + + if (!(extractorByFileExtension instanceof TsExtractor)) { + TsExtractor tsExtractor = + createTsExtractor( + payloadReaderFactoryFlags, + exposeCea608WhenMissingDeclarations, + format, + muxedCaptionFormats, + timestampAdjuster); + if (sniffQuietly(tsExtractor, extractorInput)) { + return buildResult(tsExtractor); + } + } + + // Fall back on the extractor created by file extension. + return buildResult(extractorByFileExtension); + } + + private Extractor createExtractorByFileExtension( + Uri uri, + Format format, + @Nullable List<Format> muxedCaptionFormats, + TimestampAdjuster timestampAdjuster) { + String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + lastPathSegment = ""; + } + if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType) + || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + return new WebvttExtractor(format.language, timestampAdjuster); + } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + return new AdtsExtractor(); + } 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) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) + || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) + || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { + return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + } else { + // For any other file extension, we assume TS format. + return createTsExtractor( + payloadReaderFactoryFlags, + exposeCea608WhenMissingDeclarations, + format, + muxedCaptionFormats, + timestampAdjuster); + } + } + + private static TsExtractor createTsExtractor( + @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags, + boolean exposeCea608WhenMissingDeclarations, + Format format, + @Nullable List<Format> muxedCaptionFormats, + TimestampAdjuster timestampAdjuster) { + @DefaultTsPayloadReaderFactory.Flags + int payloadReaderFactoryFlags = + DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM + | userProvidedPayloadReaderFactoryFlags; + if (muxedCaptionFormats != null) { + // The playlist declares closed caption renditions, we should ignore descriptors. + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } else if (exposeCea608WhenMissingDeclarations) { + // The playlist does not provide any closed caption information. We preemptively declare a + // closed caption track on channel 0. + muxedCaptionFormats = + Collections.singletonList( + Format.createTextSampleFormat( + /* id= */ null, + MimeTypes.APPLICATION_CEA608, + /* selectionFlags= */ 0, + /* language= */ null)); + } 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))) { + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } + + return new TsExtractor( + TsExtractor.MODE_HLS, + timestampAdjuster, + new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats)); + } + + private static FragmentedMp4Extractor createFragmentedMp4Extractor( + TimestampAdjuster timestampAdjuster, + Format format, + @Nullable List<Format> muxedCaptionFormats) { + // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid + // creating a separate EMSG track for every audio track in a video stream. + return new FragmentedMp4Extractor( + /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, + timestampAdjuster, + /* sideloadedTrack= */ null, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); + } + + /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */ + private static boolean isFmp4Variant(Format format) { + Metadata metadata = format.metadata; + if (metadata == null) { + return false; + } + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof HlsTrackMetadataEntry) { + return !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty(); + } + } + return false; + } + + @Nullable + private static Result buildResultForSameExtractorType( + Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) { + if (previousExtractor instanceof WebvttExtractor) { + return buildResult(new WebvttExtractor(format.language, timestampAdjuster)); + } else if (previousExtractor instanceof AdtsExtractor) { + 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 { + return null; + } + } + + private static Result buildResult(Extractor extractor) { + return new Result( + extractor, + extractor instanceof AdtsExtractor + || extractor instanceof Ac3Extractor + || extractor instanceof Ac4Extractor + || extractor instanceof Mp3Extractor, + isReusable(extractor)); + } + + private static boolean sniffQuietly(Extractor extractor, ExtractorInput input) + throws InterruptedException, IOException { + boolean result = false; + try { + result = extractor.sniff(input); + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); + } + return result; + } + + private static boolean isReusable(Extractor previousExtractor) { + return previousExtractor instanceof TsExtractor + || previousExtractor instanceof FragmentedMp4Extractor; + } +} |