| /* |
| * Copyright 2020 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; |
| |
| import android.content.Context; |
| import android.net.Uri; |
| import android.util.SparseArray; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.ExoPlayerLibraryInfo; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.MediaItem; |
| import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; |
| import com.google.android.exoplayer2.drm.DrmSessionManager; |
| import com.google.android.exoplayer2.drm.FrameworkMediaDrm; |
| import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; |
| import com.google.android.exoplayer2.drm.MediaDrmCallback; |
| import com.google.android.exoplayer2.offline.StreamKey; |
| import com.google.android.exoplayer2.upstream.DataSource; |
| import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; |
| import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; |
| import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; |
| import com.google.android.exoplayer2.upstream.HttpDataSource; |
| import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.MimeTypes; |
| import com.google.android.exoplayer2.util.Util; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The default {@link MediaSourceFactory} implementation. |
| * |
| * <p>This implementation delegates calls to {@link #createMediaSource(MediaItem)} to the following |
| * factories: |
| * |
| * <ul> |
| * <li>{@code DashMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#sourceUri |
| * sourceUri} ends in '.mpd' or if its {@link MediaItem.PlaybackProperties#mimeType mimeType |
| * field} is explicitly set to {@link MimeTypes#APPLICATION_MPD} (Requires the <a |
| * href="https://exoplayer.dev/hello-world.html#add-exoplayer-modules">exoplayer-dash module |
| * to be added</a> to the app). |
| * <li>{@code HlsMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#sourceUri |
| * sourceUri} ends in '.m3u8' or if its {@link MediaItem.PlaybackProperties#mimeType mimeType |
| * field} is explicitly set to {@link MimeTypes#APPLICATION_M3U8} (Requires the <a |
| * href="https://exoplayer.dev/hello-world.html#add-exoplayer-modules">exoplayer-hls module to |
| * be added</a> to the app). |
| * <li>{@code SsMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#sourceUri |
| * sourceUri} ends in '.ism', '.ism/Manifest' or if its {@link |
| * MediaItem.PlaybackProperties#mimeType mimeType field} is explicitly set to {@link |
| * MimeTypes#APPLICATION_SS} (Requires the <a |
| * href="https://exoplayer.dev/hello-world.html#add-exoplayer-modules"> |
| * exoplayer-smoothstreaming module to be added</a> to the app). |
| * <li>{@link ProgressiveMediaSource.Factory} serves as a fallback if the item's {@link |
| * MediaItem.PlaybackProperties#sourceUri sourceUri} doesn't match one of the above. It tries |
| * to infer the required extractor by using the {@link |
| * com.google.android.exoplayer2.extractor.DefaultExtractorsFactory}. An {@link |
| * UnrecognizedInputFormatException} is thrown if none of the available extractors can read |
| * the stream. |
| * </ul> |
| * |
| * <h3>DrmSessionManager creation for protected content</h3> |
| * |
| * <p>For a media item with a valid {@link |
| * com.google.android.exoplayer2.MediaItem.DrmConfiguration}, a {@link DefaultDrmSessionManager} is |
| * created. The following setter can be used to optionally configure the creation: |
| * |
| * <ul> |
| * <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory |
| * to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link |
| * DefaultHttpDataSourceFactory}). |
| * </ul> |
| * |
| * <p>For media items without a drm configuration {@link DrmSessionManager#DUMMY} is used. To use an |
| * alternative dummy, apps can pass a drm session manager to {@link |
| * #setDrmSessionManager(DrmSessionManager)} which will be used for all items without a drm |
| * configuration. |
| */ |
| public final class DefaultMediaSourceFactory implements MediaSourceFactory { |
| |
| /** |
| * Creates a new instance with the given {@link Context}. |
| * |
| * <p>This is functionally equivalent with calling {@code #newInstance(Context, |
| * DefaultDataSourceFactory)}. |
| * |
| * @param context The {@link Context}. |
| */ |
| public static DefaultMediaSourceFactory newInstance(Context context) { |
| return newInstance( |
| context, |
| new DefaultDataSourceFactory( |
| context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); |
| } |
| |
| /** |
| * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}. |
| * |
| * @param context The {@link Context}. |
| * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. |
| */ |
| public static DefaultMediaSourceFactory newInstance( |
| Context context, DataSource.Factory dataSourceFactory) { |
| return new DefaultMediaSourceFactory(context, dataSourceFactory); |
| } |
| |
| private final DataSource.Factory dataSourceFactory; |
| private final SparseArray<MediaSourceFactory> mediaSourceFactories; |
| @C.ContentType private final int[] supportedTypes; |
| private final String userAgent; |
| |
| private DrmSessionManager drmSessionManager; |
| private HttpDataSource.Factory drmHttpDataSourceFactory; |
| @Nullable private List<StreamKey> streamKeys; |
| |
| private DefaultMediaSourceFactory(Context context, DataSource.Factory dataSourceFactory) { |
| this.dataSourceFactory = dataSourceFactory; |
| drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); |
| userAgent = Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY); |
| drmHttpDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); |
| mediaSourceFactories = loadDelegates(dataSourceFactory); |
| supportedTypes = new int[mediaSourceFactories.size()]; |
| for (int i = 0; i < mediaSourceFactories.size(); i++) { |
| supportedTypes[i] = mediaSourceFactories.keyAt(i); |
| } |
| } |
| |
| /** |
| * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback |
| * HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null} |
| * is passed the {@link DefaultHttpDataSourceFactory} is used. |
| * |
| * @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link |
| * DefaultHttpDataSourceFactory}. |
| * @return This factory, for convenience. |
| */ |
| public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( |
| @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { |
| this.drmHttpDataSourceFactory = |
| drmHttpDataSourceFactory != null |
| ? drmHttpDataSourceFactory |
| : new DefaultHttpDataSourceFactory(userAgent); |
| return this; |
| } |
| |
| @Override |
| public DefaultMediaSourceFactory setDrmSessionManager( |
| @Nullable DrmSessionManager drmSessionManager) { |
| this.drmSessionManager = |
| drmSessionManager != null |
| ? drmSessionManager |
| : DrmSessionManager.getDummyDrmSessionManager(); |
| return this; |
| } |
| |
| @Override |
| public DefaultMediaSourceFactory setLoadErrorHandlingPolicy( |
| @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { |
| LoadErrorHandlingPolicy newLoadErrorHandlingPolicy = |
| loadErrorHandlingPolicy != null |
| ? loadErrorHandlingPolicy |
| : new DefaultLoadErrorHandlingPolicy(); |
| for (int i = 0; i < mediaSourceFactories.size(); i++) { |
| mediaSourceFactories.valueAt(i).setLoadErrorHandlingPolicy(newLoadErrorHandlingPolicy); |
| } |
| return this; |
| } |
| |
| /** |
| * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link |
| * #createMediaSource(MediaItem)} instead. |
| */ |
| @SuppressWarnings("deprecation") |
| @Deprecated |
| @Override |
| public DefaultMediaSourceFactory setStreamKeys(@Nullable List<StreamKey> streamKeys) { |
| this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null; |
| return this; |
| } |
| |
| @Override |
| public int[] getSupportedTypes() { |
| return Arrays.copyOf(supportedTypes, supportedTypes.length); |
| } |
| |
| @SuppressWarnings("deprecation") |
| @Override |
| public MediaSource createMediaSource(MediaItem mediaItem) { |
| Assertions.checkNotNull(mediaItem.playbackProperties); |
| @C.ContentType |
| int type = |
| inferContentType( |
| mediaItem.playbackProperties.sourceUri, mediaItem.playbackProperties.mimeType); |
| @Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type); |
| Assertions.checkNotNull( |
| mediaSourceFactory, "No suitable media source factory found for content type: " + type); |
| mediaSourceFactory.setDrmSessionManager(createDrmSessionManager(mediaItem)); |
| mediaSourceFactory.setStreamKeys( |
| !mediaItem.playbackProperties.streamKeys.isEmpty() |
| ? mediaItem.playbackProperties.streamKeys |
| : streamKeys); |
| |
| MediaSource leafMediaSource = mediaSourceFactory.createMediaSource(mediaItem); |
| |
| List<MediaItem.Subtitle> subtitles = mediaItem.playbackProperties.subtitles; |
| if (subtitles.isEmpty()) { |
| return maybeClipMediaSource(mediaItem, leafMediaSource); |
| } |
| |
| MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1]; |
| mediaSources[0] = leafMediaSource; |
| SingleSampleMediaSource.Factory singleSampleSourceFactory = |
| new SingleSampleMediaSource.Factory(dataSourceFactory); |
| for (int i = 0; i < subtitles.size(); i++) { |
| MediaItem.Subtitle subtitle = subtitles.get(i); |
| Format subtitleFormat = |
| new Format.Builder() |
| .setSampleMimeType(subtitle.mimeType) |
| .setLanguage(subtitle.language) |
| .setSelectionFlags(subtitle.selectionFlags) |
| .build(); |
| mediaSources[i + 1] = |
| singleSampleSourceFactory.createMediaSource( |
| subtitle.uri, subtitleFormat, /* durationUs= */ C.TIME_UNSET); |
| } |
| return maybeClipMediaSource(mediaItem, new MergingMediaSource(mediaSources)); |
| } |
| |
| // internal methods |
| |
| private DrmSessionManager createDrmSessionManager(MediaItem mediaItem) { |
| Assertions.checkNotNull(mediaItem.playbackProperties); |
| if (mediaItem.playbackProperties.drmConfiguration == null |
| || mediaItem.playbackProperties.drmConfiguration.licenseUri == null |
| || Util.SDK_INT < 18) { |
| return drmSessionManager; |
| } |
| return new DefaultDrmSessionManager.Builder() |
| .setUuidAndExoMediaDrmProvider( |
| mediaItem.playbackProperties.drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) |
| .setMultiSession(mediaItem.playbackProperties.drmConfiguration.multiSession) |
| .setPlayClearSamplesWithoutKeys( |
| mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey) |
| .setUseDrmSessionsForClearContent( |
| Util.toArray(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes)) |
| .build(createHttpMediaDrmCallback(mediaItem.playbackProperties.drmConfiguration)); |
| } |
| |
| private MediaDrmCallback createHttpMediaDrmCallback(MediaItem.DrmConfiguration drmConfiguration) { |
| Assertions.checkNotNull(drmConfiguration.licenseUri); |
| HttpMediaDrmCallback drmCallback = |
| new HttpMediaDrmCallback(drmConfiguration.licenseUri.toString(), drmHttpDataSourceFactory); |
| for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) { |
| drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue()); |
| } |
| return drmCallback; |
| } |
| |
| private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) { |
| if (mediaItem.clippingProperties.startPositionMs == 0 |
| && mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE |
| && !mediaItem.clippingProperties.relativeToDefaultPosition) { |
| return mediaSource; |
| } |
| return new ClippingMediaSource( |
| mediaSource, |
| C.msToUs(mediaItem.clippingProperties.startPositionMs), |
| C.msToUs(mediaItem.clippingProperties.endPositionMs), |
| /* enableInitialDiscontinuity= */ !mediaItem.clippingProperties.startsAtKeyFrame, |
| /* allowDynamicClippingUpdates= */ mediaItem.clippingProperties.relativeToLiveWindow, |
| mediaItem.clippingProperties.relativeToDefaultPosition); |
| } |
| |
| private static SparseArray<MediaSourceFactory> loadDelegates( |
| DataSource.Factory dataSourceFactory) { |
| SparseArray<MediaSourceFactory> factories = new SparseArray<>(); |
| // LINT.IfChange |
| try { |
| Class<? extends MediaSourceFactory> factoryClazz = |
| Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory") |
| .asSubclass(MediaSourceFactory.class); |
| factories.put( |
| C.TYPE_DASH, |
| factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); |
| } catch (Exception e) { |
| // Expected if the app was built without the dash module. |
| } |
| try { |
| Class<? extends MediaSourceFactory> factoryClazz = |
| Class.forName( |
| "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory") |
| .asSubclass(MediaSourceFactory.class); |
| factories.put( |
| C.TYPE_SS, |
| factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); |
| } catch (Exception e) { |
| // Expected if the app was built without the smoothstreaming module. |
| } |
| try { |
| Class<? extends MediaSourceFactory> factoryClazz = |
| Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory") |
| .asSubclass(MediaSourceFactory.class); |
| factories.put( |
| C.TYPE_HLS, |
| factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); |
| } catch (Exception e) { |
| // Expected if the app was built without the hls module. |
| } |
| // LINT.ThenChange(../../../../../../../../proguard-rules.txt) |
| factories.put(C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory)); |
| return factories; |
| } |
| |
| private static int inferContentType(Uri sourceUri, @Nullable String mimeType) { |
| if (mimeType == null) { |
| return Util.inferContentType(sourceUri); |
| } |
| switch (mimeType) { |
| case MimeTypes.APPLICATION_MPD: |
| return C.TYPE_DASH; |
| case MimeTypes.APPLICATION_M3U8: |
| return C.TYPE_HLS; |
| case MimeTypes.APPLICATION_SS: |
| return C.TYPE_SS; |
| default: |
| return Util.inferContentType(sourceUri); |
| } |
| } |
| } |