/* * Copyright (C) 2016 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.upstream; import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * A {@link DataSource} that supports multiple URI schemes. The supported schemes are: * * <ul> * <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is * a local file URI). * <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). * <li>rawresource: For fetching data from a raw resource in the application's apk (e.g. * rawresource:///resourceId, where rawResourceId is the integer identifier of the raw * resource). * <li>content: For fetching data from a content URI (e.g. content://authority/path/123). * <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension. * <li>data: For parsing data inlined in the URI as defined in RFC 2397. * <li>udp: For fetching data over UDP (e.g. udp://something.com/media). * <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), * if constructed using {@link #DefaultDataSource(Context, TransferListener, String, * boolean)}, or any other schemes supported by a base data source if constructed using {@link * #DefaultDataSource(Context, TransferListener, DataSource)}. * </ul> */ public final class DefaultDataSource implements DataSource { private static final String TAG = "DefaultDataSource"; private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; private static final String SCHEME_UDP = "udp"; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; private final List<TransferListener> transferListeners; private final DataSource baseDataSource; // Lazily initialized. @Nullable private DataSource fileDataSource; @Nullable private DataSource assetDataSource; @Nullable private DataSource contentDataSource; @Nullable private DataSource rtmpDataSource; @Nullable private DataSource udpDataSource; @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; private @Nullable DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. * @param userAgent The User-Agent to use when requesting remote data. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ public DefaultDataSource(Context context, String userAgent, boolean allowCrossProtocolRedirects) { this( context, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); } /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. * @param userAgent The User-Agent to use when requesting remote data. * @param connectTimeoutMillis The connection timeout that should be used when requesting remote * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in * milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ public DefaultDataSource( Context context, String userAgent, int connectTimeoutMillis, int readTimeoutMillis, boolean allowCrossProtocolRedirects) { this( context, new DefaultHttpDataSource( userAgent, /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, /* defaultRequestProperties= */ null)); } /** * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other * than file, asset and content. * * @param context A context. * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and * content. This {@link DataSource} should normally support at least http(s). */ public DefaultDataSource(Context context, DataSource baseDataSource) { this.context = context.getApplicationContext(); this.baseDataSource = Assertions.checkNotNull(baseDataSource); transferListeners = new ArrayList<>(); } /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. * @param listener An optional listener. * @param userAgent The User-Agent to use when requesting remote data. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link * #addTransferListener(TransferListener)}. */ @Deprecated @SuppressWarnings("deprecation") public DefaultDataSource( Context context, @Nullable TransferListener listener, String userAgent, boolean allowCrossProtocolRedirects) { this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); } /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. * @param listener An optional listener. * @param userAgent The User-Agent to use when requesting remote data. * @param connectTimeoutMillis The connection timeout that should be used when requesting remote * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in * milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link * #addTransferListener(TransferListener)}. */ @Deprecated @SuppressWarnings("deprecation") public DefaultDataSource( Context context, @Nullable TransferListener listener, String userAgent, int connectTimeoutMillis, int readTimeoutMillis, boolean allowCrossProtocolRedirects) { this( context, listener, new DefaultHttpDataSource( userAgent, /* contentTypePredicate= */ null, listener, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, /* defaultRequestProperties= */ null)); } /** * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other * than file, asset and content. * * @param context A context. * @param listener An optional listener. * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and * content. This {@link DataSource} should normally support at least http(s). * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link * #addTransferListener(TransferListener)}. */ @Deprecated public DefaultDataSource( Context context, @Nullable TransferListener listener, DataSource baseDataSource) { this(context, baseDataSource); if (listener != null) { transferListeners.add(listener); } } @Override public void addTransferListener(TransferListener transferListener) { baseDataSource.addTransferListener(transferListener); transferListeners.add(transferListener); maybeAddListenerToDataSource(fileDataSource, transferListener); maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @Override public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(dataSource == null); // Choose the correct source for the scheme. String scheme = dataSpec.uri.getScheme(); if (Util.isLocalFileUri(dataSpec.uri)) { String uriPath = dataSpec.uri.getPath(); if (uriPath != null && uriPath.startsWith("/android_asset/")) { dataSource = getAssetDataSource(); } else { dataSource = getFileDataSource(); } } else if (SCHEME_ASSET.equals(scheme)) { dataSource = getAssetDataSource(); } else if (SCHEME_CONTENT.equals(scheme)) { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); } else if (SCHEME_UDP.equals(scheme)) { dataSource = getUdpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); } else if (SCHEME_RAW.equals(scheme)) { dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; } // Open the source and return. return dataSource.open(dataSpec); } @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength); } @Override public @Nullable Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } @Override public Map<String, List<String>> getResponseHeaders() { return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders(); } @Override public void close() throws IOException { if (dataSource != null) { try { dataSource.close(); } finally { dataSource = null; } } } private DataSource getUdpDataSource() { if (udpDataSource == null) { udpDataSource = new UdpDataSource(); addListenersToDataSource(udpDataSource); } return udpDataSource; } private DataSource getFileDataSource() { if (fileDataSource == null) { fileDataSource = new FileDataSource(); addListenersToDataSource(fileDataSource); } return fileDataSource; } private DataSource getAssetDataSource() { if (assetDataSource == null) { assetDataSource = new AssetDataSource(context); addListenersToDataSource(assetDataSource); } return assetDataSource; } private DataSource getContentDataSource() { if (contentDataSource == null) { contentDataSource = new ContentDataSource(context); addListenersToDataSource(contentDataSource); } return contentDataSource; } private DataSource getRtmpDataSource() { if (rtmpDataSource == null) { try { // LINT.IfChange Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource"); rtmpDataSource = (DataSource) clazz.getConstructor().newInstance(); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) addListenersToDataSource(rtmpDataSource); } catch (ClassNotFoundException e) { // Expected if the app was built without the RTMP extension. Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension"); } catch (Exception e) { // The RTMP extension is present, but instantiation failed. throw new RuntimeException("Error instantiating RTMP extension", e); } if (rtmpDataSource == null) { rtmpDataSource = baseDataSource; } } return rtmpDataSource; } private DataSource getDataSchemeDataSource() { if (dataSchemeDataSource == null) { dataSchemeDataSource = new DataSchemeDataSource(); addListenersToDataSource(dataSchemeDataSource); } return dataSchemeDataSource; } private DataSource getRawResourceDataSource() { if (rawResourceDataSource == null) { rawResourceDataSource = new RawResourceDataSource(context); addListenersToDataSource(rawResourceDataSource); } return rawResourceDataSource; } private void addListenersToDataSource(DataSource dataSource) { for (int i = 0; i < transferListeners.size(); i++) { dataSource.addTransferListener(transferListeners.get(i)); } } private void maybeAddListenerToDataSource( @Nullable DataSource dataSource, TransferListener listener) { if (dataSource != null) { dataSource.addTransferListener(listener); } } }