diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e69bcc917..9e7a992e11 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,17 @@ # Release notes # +### 2.10.1 ### + +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). + ### 2.10.0 ### * Core library: diff --git a/constants.gradle b/constants.gradle index 5063c59141..b2ee322ee6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.0' - releaseVersionCode = 2010000 + releaseVersion = '2.10.1' + releaseVersionCode = 2010001 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index f372a47df6..a913a9b891 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ @@ -86,11 +83,9 @@ public class DownloadTracker { } @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { + public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED - ? download.request.streamKeys - : Collections.emptyList(); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; } public void toggleDownload( diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index acb24adebe..35307eb5d8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -75,7 +76,6 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -457,33 +457,26 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } @ContentType int type = Util.inferContentType(uri, overrideExtension); - List offlineStreamKeys = getOfflineStreamKeys(uri); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: { + default: throw new IllegalStateException("Unsupported type: " + type); - } } } - private List getOfflineStreamKeys(Uri uri) { - return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); - } - private DefaultDrmSessionManager buildDrmSessionManagerV18( UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index a9995af0e4..ca196b1d2f 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final CronetEngine cronetEngine; private final Executor executor; - private final Predicate contentTypePredicate; + @Nullable private final Predicate contentTypePredicate; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. + */ + public CronetDataSource(CronetEngine cronetEngine, Executor executor) { + this(cronetEngine, executor, /* contentTypePredicate= */ null); + } + /** * @param cronetEngine A CronetEngine. * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may @@ -158,7 +170,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * #open(DataSpec)}. */ public CronetDataSource( - CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) { + CronetEngine cronetEngine, + Executor executor, + @Nullable Predicate contentTypePredicate) { this( cronetEngine, executor, @@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 465ad51ac5..f1316b2bfb 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -948,8 +948,8 @@ public final class ImaAdsLoader @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { - // The player is being reset and this source will be released. + if (timeline.isEmpty()) { + // The player is being reset or contains no media. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index a749495184..8eb8bba920 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private long bytesSkipped; private long bytesRead; + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + * @param userAgent An optional User-Agent string. + */ + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { + this(callFactory, userAgent, /* contentTypePredicate= */ null); + } + /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. diff --git a/gradle.properties b/gradle.properties index 4b9bfa8fa2..31ff0ad6b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.enableUnitTestBinaryResources=true buildDir=buildout +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m diff --git a/javadoc_library.gradle b/javadoc_library.gradle index a818ea390e..74fcc3dd6c 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -18,10 +18,13 @@ android.libraryVariants.all { variant -> if (!name.equals("release")) { return; // Skip non-release builds. } + def allSourceDirs = variant.sourceSets.inject ([]) { + acc, val -> acc << val.javaDirectories + } task("generateJavadoc", type: Javadoc) { description = "Generates Javadoc for the ${javadocTitle}." title = "ExoPlayer ${javadocTitle}" - source = variant.javaCompileProvider.get().source + source = allSourceDirs options { links "http://docs.oracle.com/javase/7/docs/api/" linksOffline "https://developer.android.com/reference", diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 07ba438182..8c11810506 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -46,18 +46,21 @@ # Constructors accessed via reflection in DownloadHelper -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 37774bccb5..03c3482eac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1053,11 +1053,14 @@ import java.util.concurrent.atomic.AtomicBoolean; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { - pendingMessages.remove(nextPendingMessageIndex); - } else { - nextPendingMessageIndex++; + try { + sendMessageToTarget(nextInfo.message); + } finally { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { + pendingMessages.remove(nextPendingMessageIndex); + } else { + nextPendingMessageIndex++; + } } nextInfo = nextPendingMessageIndex < pendingMessages.size() diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 72760db31b..a90435227b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.10.0"; + public static final String VERSION = "2.10.1"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2010000; + public static final int VERSION_INT = 2010001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 910404a875..697f35e417 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1231,7 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/faqs.html#" + + "https://exoplayer.dev/troubleshooting.html#" + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 7f74216cc8..3400cf25b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -59,7 +59,7 @@ public interface AnalyticsListener { public final Timeline timeline; /** - * Window index in the {@code timeline} this event belongs to, or the prospective window index + * Window index in the {@link #timeline} this event belongs to, or the prospective window index * if the timeline is not yet known and empty. */ public final int windowIndex; @@ -76,7 +76,7 @@ public interface AnalyticsListener { public final long eventPlaybackPositionMs; /** - * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the + * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the * currently playing ad at the time of the event, in milliseconds. */ public final long currentPlaybackPositionMs; @@ -91,15 +91,15 @@ public interface AnalyticsListener { * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at * the time of the event, in milliseconds. * @param timeline Timeline at the time of the event. - * @param windowIndex Window index in the {@code timeline} this event belongs to, or the + * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the * prospective window index if the timeline is not yet known and empty. * @param mediaPeriodId Media period identifier for the media period this event belongs to, or * {@code null} if the event is not associated with a specific media period. * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time * of the event, in milliseconds. - * @param currentPlaybackPositionMs Position in the current timeline window ({@code - * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, - * in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@link + * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in + * milliseconds. * @param totalBufferedDurationMs Total buffered duration from {@link * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes * pre-buffered data for subsequent ads and windows. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 07769e7d85..e75f7ffc7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -786,7 +786,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); - if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) { mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } @@ -809,6 +809,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the device's decoders are known to not support setting the codec operating + * rate. + * + *

See GitHub issue #5821. + */ + private static boolean deviceDoesntSupportOperatingRate() { + return Util.SDK_INT == 23 + && ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL)); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 0185a6d8af..6fb0ac6856 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -60,7 +60,7 @@ import java.util.List; * The threshold number of samples to trim from the start/end of an audio track when applying an * edit below which gapless info can be used (rather than removing samples from the sample table). */ - private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index d04cd3a999..3d873926bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';"); + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 06f308d1e9..ef4bd00f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -233,6 +233,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @Override + public void setStatesToRemoving() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_REMOVING); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 8a15c82c89..755f7e0343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.Nullable; -import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -32,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -106,30 +107,13 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - @Nullable private static final Constructor DASH_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor HLS_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor SS_FACTORY_CONSTRUCTOR; - @Nullable private static final Method DASH_FACTORY_CREATE_METHOD; - @Nullable private static final Method HLS_FACTORY_CREATE_METHOD; - @Nullable private static final Method SS_FACTORY_CREATE_METHOD; - - static { - Pair<@NullableType Constructor, @NullableType Method> dashFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first; - DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> hlsFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first; - HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> ssFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; - SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; - } + private static final MediaSourceFactory DASH_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + private static final MediaSourceFactory SS_FACTORY = + getMediaSourceFactory( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + private static final MediaSourceFactory HLS_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); /** * Creates a {@link DownloadHelper} for progressive streams. @@ -202,8 +186,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD), + DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -252,8 +235,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD), + HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -302,11 +284,42 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), + SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** + * Utility method to create a MediaSource which only contains the tracks defined in {@code + * downloadRequest}. + * + * @param downloadRequest A {@link DownloadRequest}. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + MediaSourceFactory factory; + switch (downloadRequest.type) { + case DownloadRequest.TYPE_DASH: + factory = DASH_FACTORY; + break; + case DownloadRequest.TYPE_SS: + factory = SS_FACTORY; + break; + case DownloadRequest.TYPE_HLS: + factory = HLS_FACTORY; + break; + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(downloadRequest.uri); + default: + throw new IllegalStateException("Unsupported type: " + downloadRequest.type); + } + return factory.createMediaSource( + downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + } + private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; @@ -739,35 +752,54 @@ public final class DownloadHelper { } } - private static Pair<@NullableType Constructor, @NullableType Method> - getMediaSourceFactoryMethods(String className) { + private static MediaSourceFactory getMediaSourceFactory(String className) { Constructor constructor = null; + Method setStreamKeysMethod = null; Method createMethod = null; try { // LINT.IfChange Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(DataSource.Factory.class); + constructor = factoryClazz.getConstructor(Factory.class); + setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - } catch (Exception e) { + } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. + } catch (NoSuchMethodException | SecurityException e) { + // Something is wrong with the library or the proguard configuration. + throw new IllegalStateException(e); } - return Pair.create(constructor, createMethod); + return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static MediaSource createMediaSource( - Uri uri, - DataSource.Factory dataSourceFactory, - @Nullable Constructor factoryConstructor, - @Nullable Method createMediaSourceMethod) { - if (factoryConstructor == null || createMediaSourceMethod == null) { - throw new IllegalStateException("Module missing to create media source."); + private static final class MediaSourceFactory { + @Nullable private final Constructor constructor; + @Nullable private final Method setStreamKeysMethod; + @Nullable private final Method createMethod; + + public MediaSourceFactory( + @Nullable Constructor constructor, + @Nullable Method setStreamKeysMethod, + @Nullable Method createMethod) { + this.constructor = constructor; + this.setStreamKeysMethod = setStreamKeysMethod; + this.createMethod = createMethod; } - try { - Object factory = factoryConstructor.newInstance(dataSourceFactory); - return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + + private MediaSource createMediaSource( + Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { + if (constructor == null || setStreamKeysMethod == null || createMethod == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + Object factory = constructor.newInstance(dataSourceFactory); + if (streamKeys != null) { + setStreamKeysMethod.invoke(factory, streamKeys); + } + return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 3bf03dd3e8..ec5ff81d97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -133,10 +133,11 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_TASK_STOPPED = 8; - private static final int MSG_CONTENT_LENGTH_CHANGED = 9; - private static final int MSG_UPDATE_PROGRESS = 10; - private static final int MSG_RELEASE = 11; + private static final int MSG_REMOVE_ALL_DOWNLOADS = 8; + private static final int MSG_TASK_STOPPED = 9; + private static final int MSG_CONTENT_LENGTH_CHANGED = 10; + private static final int MSG_UPDATE_PROGRESS = 11; + private static final int MSG_RELEASE = 12; private static final String TAG = "DownloadManager"; @@ -446,6 +447,12 @@ public final class DownloadManager { internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); } + /** Cancels all pending downloads and removes all downloaded data. */ + public void removeAllDownloads() { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget(); + } + /** * Stops the downloads and releases resources. Waits until the downloads are persisted to the * download index. The manager must not be accessed after this method has been called. @@ -652,6 +659,9 @@ public final class DownloadManager { id = (String) message.obj; removeDownload(id); break; + case MSG_REMOVE_ALL_DOWNLOADS: + removeAllDownloads(); + break; case MSG_TASK_STOPPED: Task task = (Task) message.obj; onTaskStopped(task); @@ -797,6 +807,36 @@ public final class DownloadManager { syncTasks(); } + private void removeAllDownloads() { + List terminalDownloads = new ArrayList<>(); + try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) { + while (cursor.moveToNext()) { + terminalDownloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load downloads."); + } + for (int i = 0; i < downloads.size(); i++) { + downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING)); + } + for (int i = 0; i < terminalDownloads.size(); i++) { + downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING)); + } + Collections.sort(downloads, InternalHandler::compareStartTimes); + try { + downloadIndex.setStatesToRemoving(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + ArrayList updateList = new ArrayList<>(downloads); + for (int i = 0; i < downloads.size(); i++) { + DownloadUpdate update = + new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + syncTasks(); + } + private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); @@ -1057,16 +1097,7 @@ public final class DownloadManager { // to set STATE_STOPPED either, because it doesn't have a stopReason argument. Assertions.checkState( state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); - return putDownload( - new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - /* stopReason= */ 0, - FAILURE_REASON_NONE, - download.progress)); + return putDownload(copyDownloadWithState(download, state)); } private Download putDownload(Download download) { @@ -1120,6 +1151,18 @@ public final class DownloadManager { return C.INDEX_UNSET; } + private static Download copyDownloadWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress); + } + private static int compareStartTimes(Download first, Download second) { return Util.compareLong(first.startTimeMs, second.startTimeMs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index fdd7163a2c..3900dc8e93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -77,6 +77,16 @@ public abstract class DownloadService extends Service { public static final String ACTION_REMOVE_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** + * Removes all downloads. Extras: + * + *

    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -296,6 +306,19 @@ public abstract class DownloadService extends Service { .putExtra(KEY_CONTENT_ID, id); } + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + /** * Builds an {@link Intent} for resuming all downloads. * @@ -414,6 +437,19 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + /** * Starts the service if not started already and resumes all downloads. * @@ -560,6 +596,9 @@ public abstract class DownloadService extends Service { downloadManager.removeDownload(contentId); } break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index ae634f8544..dc7085c85e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void setDownloadingStatesToQueued() throws IOException; + /** + * Sets all states to {@link Download#STATE_REMOVING}. + * + * @throws IOException If an error occurs updating the state. + */ + void setStatesToRemoving() throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index 8572c9c7ca..e6679e1a5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.util.Util; * * * - * * } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index eb956f06db..3f2fef454f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Region; import android.util.SparseArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; @@ -150,6 +149,8 @@ import java.util.List; List cues = new ArrayList<>(); SparseArray pageRegions = subtitleService.pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); PageRegion pageRegion = pageRegions.valueAt(i); int regionId = pageRegions.keyAt(i); RegionComposition regionComposition = subtitleService.regions.get(regionId); @@ -163,9 +164,7 @@ import java.util.List; displayDefinition.horizontalPositionMaximum); int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, displayDefinition.verticalPositionMaximum); - canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, - Region.Op.REPLACE); - + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); if (clutDefinition == null) { clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); @@ -214,9 +213,11 @@ import java.util.List; (float) regionComposition.height / displayDefinition.height)); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); } - return cues; + return Collections.unmodifiableList(cues); } // Static parsing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 6aad517004..66036b7a84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private long bytesSkipped; private long bytesRead; + /** @param userAgent The User-Agent string that should be used. */ + public DefaultHttpDataSource(String userAgent) { + this(userAgent, /* contentTypePredicate= */ null); + } + /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 9cbcea5814..4602d172a6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -48,6 +48,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isNull(); } + @Test + public void decode_emptyTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEmpty(); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); @@ -70,6 +81,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isEqualTo("test_url"); } + @Test + public void decode_lineTerminatorInTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_notIcy() { IcyDecoder decoder = new IcyDecoder(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 2b9ef11235..de430d1416 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -243,6 +243,27 @@ public class DownloadManagerTest { downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test + public void removeAllDownloads_removesAllDownloads() throws Throwable { + // Finish one download and keep one running. + DownloadRunner runner1 = new DownloadRunner(uri1); + DownloadRunner runner2 = new DownloadRunner(uri2); + runner1.postDownloadRequest(); + runner1.getDownloader(0).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + runner2.postDownloadRequest(); + + runner1.postRemoveAllRequest(); + runner1.getDownloader(1).unblock(); + runner2.getDownloader(1).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + runner1.getTask().assertRemoved(); + runner2.getTask().assertRemoved(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0); + } + @Test public void differentDownloadRequestsMerged() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1); @@ -605,6 +626,11 @@ public class DownloadManagerTest { return this; } + private DownloadRunner postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + return this; + } + private DownloadRunner postDownloadRequest(StreamKey... keys) { DownloadRequest downloadRequest = new DownloadRequest( diff --git a/library/dash/README.md b/library/dash/README.md index 7831033b99..1076716684 100644 --- a/library/dash/README.md +++ b/library/dash/README.md @@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/dash.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md index 1dd1b7a62e..3470c29e3c 100644 --- a/library/hls/README.md +++ b/library/hls/README.md @@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/hls.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html 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 ef233bb566..2cfd14c79d 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 @@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - roleFlags = mediaTagFormat.roleFlags; + roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; } diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md index 4fa24543d6..d53471d17c 100644 --- a/library/smoothstreaming/README.md +++ b/library/smoothstreaming/README.md @@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/smoothstreaming.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md index 341ea2fb16..16136b3d94 100644 --- a/library/ui/README.md +++ b/library/ui/README.md @@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/ui-components.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java index 6669504c07..fd7be241df 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java @@ -27,7 +27,7 @@ import java.io.IOException; /** Fake {@link MediaChunk}. */ public final class FakeMediaChunk extends MediaChunk { - private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT"); public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs);