From dcb8417a3c6a18e636be07c44d21eddf98208639 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 09:30:25 +0100 Subject: [PATCH 001/219] Assert customCacheKey is null for DASH, HLS and SmoothStreaming downloads PiperOrigin-RevId: 243954989 --- .../exoplayer2/offline/DownloadRequest.java | 9 ++++++++- .../action_file_for_download_index_upgrade.exi | Bin 161 -> 161 bytes .../offline/ActionFileUpgradeUtilTest.java | 12 ++++++------ .../exoplayer2/offline/DownloadRequestTest.java | 5 +++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java index 5acefd6f936..7ff43ceacde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -52,7 +52,10 @@ public static class UnsupportedRequestException extends IOException {} public final Uri uri; /** Stream keys to be downloaded. If empty, all streams will be downloaded. */ public final List streamKeys; - /** Custom key for cache indexing, or null. */ + /** + * Custom key for cache indexing, or null. Must be null for DASH, HLS and SmoothStreaming + * downloads. + */ @Nullable public final String customCacheKey; /** Application defined data associated with the download. May be empty. */ public final byte[] data; @@ -72,6 +75,10 @@ public DownloadRequest( List streamKeys, @Nullable String customCacheKey, @Nullable byte[] data) { + if (TYPE_DASH.equals(type) || TYPE_HLS.equals(type) || TYPE_SS.equals(type)) { + Assertions.checkArgument( + customCacheKey == null, "customCacheKey must be null for type: " + type); + } this.id = id; this.type = type; this.uri = uri; diff --git a/library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi b/library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi index 888ba4af4467a3d7a0077afad8ea24bbd48f8be0..0bf49b133a1c91e9542671bad37539141a8f953d 100644 GIT binary patch delta 33 ecmZ3;xR8;L0Ros9SV~fhOD6JpKxyTPwJHE Date: Wed, 17 Apr 2019 11:47:48 +0100 Subject: [PATCH 002/219] Reset playback info but not position/state in release ImaAdsLoader gets the player position after the app releases the player to support resuming ads at their current position if the same ads loader is reused. PiperOrigin-RevId: 243969916 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 8e5a6d2a9b0..15deb8ea47f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -403,8 +403,8 @@ public void release() { eventHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ true, - /* resetState= */ true, + /* resetPosition= */ false, + /* resetState= */ false, /* playbackState= */ Player.STATE_IDLE); } From 721e1dbfaf8c18c4a62badf5a6aa7518e999e152 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 13:32:17 +0100 Subject: [PATCH 003/219] Add WritableDownloadIndex interface One goal we forgot about a little bit was to allow applications to provide their own index implementation. This requires the writable side to also be defined by an interface. PiperOrigin-RevId: 243979660 --- .../offline/DefaultDownloadIndex.java | 36 ++--------- .../exoplayer2/offline/DownloadIndex.java | 2 +- .../exoplayer2/offline/DownloadManager.java | 15 +++-- .../offline/WritableDownloadIndex.java | 59 +++++++++++++++++++ 4 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java 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 d7ab4201a57..fc1518e5c32 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 @@ -38,7 +38,7 @@ *

Database access may take a long time, do not call methods of this class from * the application main thread. */ -public final class DefaultDownloadIndex implements DownloadIndex { +public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads"; @@ -185,12 +185,7 @@ public DownloadCursor getDownloads(@Download.State int... states) throws Databas return new DownloadCursorImpl(cursor); } - /** - * Adds or replaces a {@link Download}. - * - * @param download The {@link Download} to be added. - * @throws DatabaseIOException If an error occurs setting the state. - */ + @Override public void putDownload(Download download) throws DatabaseIOException { ensureInitialized(); ContentValues values = new ContentValues(); @@ -218,12 +213,7 @@ public void putDownload(Download download) throws DatabaseIOException { } } - /** - * Removes the {@link Download} with the given {@code id}. - * - * @param id ID of a {@link Download}. - * @throws DatabaseIOException If an error occurs removing the state. - */ + @Override public void removeDownload(String id) throws DatabaseIOException { ensureInitialized(); try { @@ -233,13 +223,7 @@ public void removeDownload(String id) throws DatabaseIOException { } } - /** - * Sets the manual stop reason of the downloads in a terminal state ({@link - * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). - * - * @param manualStopReason The manual stop reason. - * @throws DatabaseIOException If an error occurs updating the state. - */ + @Override public void setManualStopReason(int manualStopReason) throws DatabaseIOException { ensureInitialized(); try { @@ -252,17 +236,7 @@ public void setManualStopReason(int manualStopReason) throws DatabaseIOException } } - /** - * Sets the manual stop reason of the download with the given {@code id} in a terminal state - * ({@link Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). - * - *

If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, - * then nothing happens. - * - * @param id ID of a {@link Download}. - * @param manualStopReason The manual stop reason. - * @throws DatabaseIOException If an error occurs updating the state. - */ + @Override public void setManualStopReason(String id, int manualStopReason) throws DatabaseIOException { ensureInitialized(); try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java index 90d0fa1b516..3de1b7b2121 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -18,7 +18,7 @@ import androidx.annotation.Nullable; import java.io.IOException; -/** Persists {@link Download}s. */ +/** An index of {@link Download Downloads}. */ public interface DownloadIndex { /** 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 03c33b6aad0..fdb3ca1840b 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 @@ -34,7 +34,6 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; @@ -155,7 +154,7 @@ default void onRequirementsStateChanged( private final int maxSimultaneousDownloads; private final int minRetryCount; private final Context context; - private final DefaultDownloadIndex downloadIndex; + private final WritableDownloadIndex downloadIndex; private final DownloaderFactory downloaderFactory; private final Handler mainHandler; private final HandlerThread internalThread; @@ -231,7 +230,7 @@ public DownloadManager( * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param downloadIndex The {@link DefaultDownloadIndex} that holds the downloads. + * @param downloadIndex The download index used to hold the download information. * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param minRetryCount The minimum number of times a download must be retried before failing. @@ -239,7 +238,7 @@ public DownloadManager( */ public DownloadManager( Context context, - DefaultDownloadIndex downloadIndex, + WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory, int maxSimultaneousDownloads, int minRetryCount, @@ -651,7 +650,7 @@ private void setManualStopReasonInternal(@Nullable String id, int manualStopReas } else { downloadIndex.setManualStopReason(manualStopReason); } - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "setManualStopReason failed", e); } } @@ -734,7 +733,7 @@ private void onDownloadChangedInternal(DownloadInternal downloadInternal, Downlo logd("Download state is changed", downloadInternal); try { downloadIndex.putDownload(download); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "Failed to update index", e); } if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { @@ -747,7 +746,7 @@ private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Downlo logd("Download is removed", downloadInternal); try { downloadIndex.removeDownload(download.request.id); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "Failed to remove from index", e); } downloadInternals.remove(downloadInternal); @@ -805,7 +804,7 @@ private DownloadInternal getDownload(String id) { private Download loadDownload(String id) { try { return downloadIndex.getDownload(id); - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "loadDownload failed", e); } return null; 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 new file mode 100644 index 00000000000..24f4421bc43 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 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.offline; + +import java.io.IOException; + +/** An writable index of {@link Download Downloads}. */ +public interface WritableDownloadIndex extends DownloadIndex { + + /** + * Adds or replaces a {@link Download}. + * + * @param download The {@link Download} to be added. + * @throws throws IOException If an error occurs setting the state. + */ + void putDownload(Download download) throws IOException; + + /** + * Removes the {@link Download} with the given {@code id}. + * + * @param id ID of a {@link Download}. + * @throws throws IOException If an error occurs removing the state. + */ + void removeDownload(String id) throws IOException; + /** + * Sets the manual stop reason of the downloads in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * + * @param manualStopReason The manual stop reason. + * @throws throws IOException If an error occurs updating the state. + */ + void setManualStopReason(int manualStopReason) throws IOException; + + /** + * Sets the manual stop reason of the download with the given {@code id} in a terminal state + * ({@link Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * + *

If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, + * then nothing happens. + * + * @param id ID of a {@link Download}. + * @param manualStopReason The manual stop reason. + * @throws throws IOException If an error occurs updating the state. + */ + void setManualStopReason(String id, int manualStopReason) throws IOException; +} From e15e6212f25d531c2f1403876e463dc645e4ab29 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 14:45:18 +0100 Subject: [PATCH 004/219] Fix playback of badly clipped MP3 streams Issue: #5772 PiperOrigin-RevId: 243987497 --- RELEASENOTES.md | 6 ++++-- .../exoplayer2/extractor/mp3/Mp3Extractor.java | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 765244ac1af..182701ec34c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,14 +32,16 @@ replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). * Extractors: - * MP3: Add support for SHOUTcast ICY metadata - ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * MP4/FMP4: Add support for Dolby Vision. * MP4: Fix issue handling meta atoms in some streams ([#5698](https://github.com/google/ExoPlayer/issues/5698), [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * MP3: Fix ID3 frame unsychronization ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default (i.e. if the flag is not set), the 0x82 elementary stream type is now treated as an SCTE subtitle track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 4db715f53eb..c65ad0bc670 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -341,9 +341,19 @@ private boolean synchronize(ExtractorInput input, boolean sniffing) */ private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) throws IOException, InterruptedException { - return (seeker != null && extractorInput.getPeekPosition() == seeker.getDataEndPosition()) - || !extractorInput.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + if (seeker != null) { + long dataEndPosition = seeker.getDataEndPosition(); + if (dataEndPosition != C.POSITION_UNSET + && extractorInput.getPeekPosition() > dataEndPosition - 4) { + return true; + } + } + try { + return !extractorInput.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + } catch (EOFException e) { + return true; + } } /** From c1246937dfac4fd047ae9ab48606b33aeee1ac34 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 17 Apr 2019 14:50:40 +0100 Subject: [PATCH 005/219] Upgrade IMA to 3.11.2 PiperOrigin-RevId: 243988105 --- extensions/ima/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index c80fb26124a..a91bbbd981a 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,9 +32,9 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.9' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'com.google.android.gms:play-services-ads:17.2.0' + implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From 2907f79e69633b7739ae7b48a225572abf54a7b7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 16:46:42 +0100 Subject: [PATCH 006/219] Don't start download if user explicitly deselects all tracks PiperOrigin-RevId: 244003817 --- .../android/exoplayer2/demo/DownloadTracker.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 34282fc3893..4a7a810314a 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 @@ -240,7 +240,12 @@ public void onClick(DialogInterface dialog, int which) { } } } - startDownload(); + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); } // DialogInterface.OnDismissListener implementation. @@ -254,9 +259,16 @@ public void onDismiss(DialogInterface dialogInterface) { // Internal methods. private void startDownload() { - DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { DownloadService.startWithNewDownload( context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + } } } From afd72839dceeaad8e99940a0710181782ab51f03 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 16:59:11 +0100 Subject: [PATCH 007/219] Disable cache span touching for offline Currently SimpleCache will touch cache spans whenever it reads from them. With legacy SimpleCache setups this involves a potentially expensive file rename. With new SimpleCache setups it involves a more efficient but still non-free database write. For offline use cases, and more generally any use case where the eviction policy doesn't use last access timestamps, touching is not useful. This change allows the evictor to specify whether it needs cache spans to be touched or not. SimpleCache will only touch spans if the evictor requires it. Note: There is a potential change in behavior in cases where a cache uses an evictor that doesn't need cache spans to be touched, but then later switches to an evictor that does. The new evictor may temporarily make sub-optimal eviction decisions as a result. I think this is a very fair trade-off, since this scenario is unlikely to occur much, if at all, in practice, and even if it does occur the result isn't that bad. PiperOrigin-RevId: 244005682 --- .../exoplayer2/upstream/cache/Cache.java | 11 ++++---- .../upstream/cache/CacheEvictor.java | 7 +++++ .../upstream/cache/CacheFileMetadata.java | 6 ++-- .../cache/CacheFileMetadataIndex.java | 18 ++++++------ .../exoplayer2/upstream/cache/CacheSpan.java | 15 +++++----- .../upstream/cache/CachedContent.java | 18 ++++++------ .../cache/LeastRecentlyUsedCacheEvictor.java | 11 ++++++-- .../upstream/cache/NoOpCacheEvictor.java | 5 ++++ .../upstream/cache/SimpleCache.java | 27 ++++++++++-------- .../upstream/cache/SimpleCacheSpan.java | 28 +++++++++---------- .../cache/CachedContentIndexTest.java | 4 +-- .../cache/CachedRegionTrackerTest.java | 4 +-- .../upstream/cache/SimpleCacheSpanTest.java | 21 ++++++-------- 13 files changed, 96 insertions(+), 79 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 91349e9284a..12905f908c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -49,19 +49,18 @@ interface Listener { void onSpanRemoved(Cache cache, CacheSpan span); /** - * Called when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new + * Called when an existing {@link CacheSpan} is touched, causing it to be replaced. The new * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however - * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed. - *

- * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and - * {@link #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. + * {@link CacheSpan#file} and {@link CacheSpan#lastTouchTimestamp} may have changed. + * + *

Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and {@link + * #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. * * @param cache The source of the event. * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache. * @param newSpan The new {@link CacheSpan}, which has been added to the cache. */ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); - } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java index dbec4b78fc6..6ebfe01df41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java @@ -23,6 +23,13 @@ */ public interface CacheEvictor extends Cache.Listener { + /** + * Returns whether the evictor requires the {@link Cache} to touch {@link CacheSpan CacheSpans} + * when it accesses them. Implementations that do not use {@link CacheSpan#lastTouchTimestamp} + * should return {@code false}. + */ + boolean requiresCacheSpanTouches(); + /** * Called when cache has been initialized. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java index 492b98a0de4..7ac80325a5c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java @@ -19,10 +19,10 @@ /* package */ final class CacheFileMetadata { public final long length; - public final long lastAccessTimestamp; + public final long lastTouchTimestamp; - public CacheFileMetadata(long length, long lastAccessTimestamp) { + public CacheFileMetadata(long length, long lastTouchTimestamp) { this.length = length; - this.lastAccessTimestamp = lastAccessTimestamp; + this.lastTouchTimestamp = lastTouchTimestamp; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index 084c02b11bb..027172e090d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -36,17 +36,17 @@ private static final String COLUMN_NAME = "name"; private static final String COLUMN_LENGTH = "length"; - private static final String COLUMN_LAST_ACCESS_TIMESTAMP = "last_access_timestamp"; + private static final String COLUMN_LAST_TOUCH_TIMESTAMP = "last_touch_timestamp"; private static final int COLUMN_INDEX_NAME = 0; private static final int COLUMN_INDEX_LENGTH = 1; - private static final int COLUMN_INDEX_LAST_ACCESS_TIMESTAMP = 2; + private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; private static final String[] COLUMNS = new String[] { - COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP, + COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP, }; private static final String TABLE_SCHEMA = "(" @@ -54,7 +54,7 @@ + " TEXT PRIMARY KEY NOT NULL," + COLUMN_LENGTH + " INTEGER NOT NULL," - + COLUMN_LAST_ACCESS_TIMESTAMP + + COLUMN_LAST_TOUCH_TIMESTAMP + " INTEGER NOT NULL)"; private final DatabaseProvider databaseProvider; @@ -141,8 +141,8 @@ public Map getAll() throws DatabaseIOException { while (cursor.moveToNext()) { String name = cursor.getString(COLUMN_INDEX_NAME); long length = cursor.getLong(COLUMN_INDEX_LENGTH); - long lastAccessTimestamp = cursor.getLong(COLUMN_INDEX_LAST_ACCESS_TIMESTAMP); - fileMetadata.put(name, new CacheFileMetadata(length, lastAccessTimestamp)); + long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP); + fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp)); } return fileMetadata; } catch (SQLException e) { @@ -155,17 +155,17 @@ public Map getAll() throws DatabaseIOException { * * @param name The name of the file. * @param length The file length. - * @param lastAccessTimestamp The file last access timestamp. + * @param lastTouchTimestamp The file last touch timestamp. * @throws DatabaseIOException If an error occurs setting the metadata. */ - public void set(String name, long length, long lastAccessTimestamp) throws DatabaseIOException { + public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(COLUMN_NAME, name); values.put(COLUMN_LENGTH, length); - values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp); + values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); } catch (SQLException e) { throw new DatabaseIOException(e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 7dbcd4a9227..1e8cf1517d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -45,13 +45,12 @@ public class CacheSpan implements Comparable { * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ public final @Nullable File file; - /** - * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. - */ - public final long lastAccessTimestamp; + /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ + public final long lastTouchTimestamp; /** - * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated. + * Creates a hole CacheSpan which isn't cached, has no last touch timestamp and no file + * associated. * * @param key The cache key that uniquely identifies the original stream. * @param position The position of the {@link CacheSpan} in the original stream. @@ -69,18 +68,18 @@ public CacheSpan(String key, long position, long length) { * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ public CacheSpan( - String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { this.key = key; this.position = position; this.length = length; this.isCached = file != null; this.file = file; - this.lastAccessTimestamp = lastAccessTimestamp; + this.lastTouchTimestamp = lastTouchTimestamp; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index e244163bc85..7abb9b3896d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -141,30 +141,30 @@ public long getCachedBytesLength(long position, long length) { } /** - * Sets the given span's last access timestamp. The passed span becomes invalid after this call. + * Sets the given span's last touch timestamp. The passed span becomes invalid after this call. * * @param cacheSpan Span to be copied and updated. - * @param lastAccessTimestamp The new last access timestamp. + * @param lastTouchTimestamp The new last touch timestamp. * @param updateFile Whether the span file should be renamed to have its timestamp match the new - * last access time. - * @return A span with the updated last access timestamp. + * last touch time. + * @return A span with the updated last touch timestamp. */ - public SimpleCacheSpan setLastAccessTimestamp( - SimpleCacheSpan cacheSpan, long lastAccessTimestamp, boolean updateFile) { + public SimpleCacheSpan setLastTouchTimestamp( + SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { Assertions.checkState(cachedSpans.remove(cacheSpan)); File file = cacheSpan.file; if (updateFile) { File directory = file.getParentFile(); long position = cacheSpan.position; - File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastAccessTimestamp); + File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp); if (file.renameTo(newFile)) { file = newFile; } else { - Log.w(TAG, "Failed to rename " + file + " to " + newFile + "."); + Log.w(TAG, "Failed to rename " + file + " to " + newFile); } } SimpleCacheSpan newCacheSpan = - cacheSpan.copyWithFileAndLastAccessTimestamp(file, lastAccessTimestamp); + cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp); cachedSpans.add(newCacheSpan); return newCacheSpan; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index aa40c1d2fd8..44a735f144d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -35,6 +35,11 @@ public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.leastRecentlyUsed = new TreeSet<>(this); } + @Override + public boolean requiresCacheSpanTouches() { + return true; + } + @Override public void onCacheInitialized() { // Do nothing. @@ -68,12 +73,12 @@ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { @Override public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp; - if (lastAccessTimestampDelta == 0) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { // Use the standard compareTo method as a tie-break. return lhs.compareTo(rhs); } - return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1; + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; } private void evictCache(Cache cache, long requiredSpace) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java index b0c8c7e0876..da89dc1cb32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java @@ -24,6 +24,11 @@ */ public final class NoOpCacheEvictor implements CacheEvictor { + @Override + public boolean requiresCacheSpanTouches() { + return false; + } + @Override public void onCacheInitialized() { // Do nothing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 14f659855b2..b31d3b66f33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -70,6 +70,7 @@ public final class SimpleCache implements Cache { @Nullable private final CacheFileMetadataIndex fileIndex; private final HashMap> listeners; private final Random random; + private final boolean touchCacheSpans; private long uid; private long totalSpace; @@ -279,6 +280,7 @@ public SimpleCache( this.fileIndex = fileIndex; listeners = new HashMap<>(); random = new Random(); + touchCacheSpans = evictor.requiresCacheSpanTouches(); uid = UID_UNSET; // Start cache initialization. @@ -408,23 +410,26 @@ public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long p // Read case. if (span.isCached) { + if (!touchCacheSpans) { + return span; + } String fileName = Assertions.checkNotNull(span.file).getName(); long length = span.length; - long lastAccessTimestamp = System.currentTimeMillis(); + long lastTouchTimestamp = System.currentTimeMillis(); boolean updateFile = false; if (fileIndex != null) { try { - fileIndex.set(fileName, length, lastAccessTimestamp); + fileIndex.set(fileName, length, lastTouchTimestamp); } catch (IOException e) { - throw new CacheException(e); + Log.w(TAG, "Failed to update index with new touch timestamp."); } } else { - // Updating the file itself to incorporate the new last access timestamp is much slower than + // Updating the file itself to incorporate the new last touch timestamp is much slower than // updating the file index. Hence we only update the file if we don't have a file index. updateFile = true; } SimpleCacheSpan newSpan = - contentIndex.get(key).setLastAccessTimestamp(span, lastAccessTimestamp, updateFile); + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); notifySpanTouched(span, newSpan); return newSpan; } @@ -459,8 +464,8 @@ public synchronized File startFile(String key, long position, long length) throw if (!fileDir.exists()) { fileDir.mkdir(); } - long lastAccessTimestamp = System.currentTimeMillis(); - return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastAccessTimestamp); + long lastTouchTimestamp = System.currentTimeMillis(); + return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastTouchTimestamp); } @Override @@ -488,7 +493,7 @@ public synchronized void commitFile(File file, long length) throws CacheExceptio if (fileIndex != null) { String fileName = file.getName(); try { - fileIndex.set(fileName, span.length, span.lastAccessTimestamp); + fileIndex.set(fileName, span.length, span.lastTouchTimestamp); } catch (IOException e) { throw new CacheException(e); } @@ -674,14 +679,14 @@ private void loadDirectory( continue; } long length = C.LENGTH_UNSET; - long lastAccessTimestamp = C.TIME_UNSET; + long lastTouchTimestamp = C.TIME_UNSET; CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null; if (metadata != null) { length = metadata.length; - lastAccessTimestamp = metadata.lastAccessTimestamp; + lastTouchTimestamp = metadata.lastTouchTimestamp; } SimpleCacheSpan span = - SimpleCacheSpan.createCacheEntry(file, length, lastAccessTimestamp, contentIndex); + SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex); if (span != null) { addSpan(span); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 72358300192..7d9f0c9ff12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -96,7 +96,7 @@ public static SimpleCacheSpan createClosedHole(String key, long position, long l */ @Nullable public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) { - return createCacheEntry(file, length, /* lastAccessTimestamp= */ C.TIME_UNSET, index); + return createCacheEntry(file, length, /* lastTouchTimestamp= */ C.TIME_UNSET, index); } /** @@ -106,14 +106,14 @@ public static SimpleCacheSpan createCacheEntry(File file, long length, CachedCon * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the * underlying file system. Querying the underlying file system can be expensive, so callers * that already know the length of the file should pass it explicitly. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} to use the file + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file * timestamp. * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index, or if the length is 0. */ @Nullable public static SimpleCacheSpan createCacheEntry( - File file, long length, long lastAccessTimestamp, CachedContentIndex index) { + File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { file = upgradeFile(file, index); @@ -142,10 +142,10 @@ public static SimpleCacheSpan createCacheEntry( } long position = Long.parseLong(matcher.group(2)); - if (lastAccessTimestamp == C.TIME_UNSET) { - lastAccessTimestamp = Long.parseLong(matcher.group(3)); + if (lastTouchTimestamp == C.TIME_UNSET) { + lastTouchTimestamp = Long.parseLong(matcher.group(3)); } - return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } /** @@ -187,26 +187,26 @@ private static File upgradeFile(File file, CachedContentIndex index) { * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ private SimpleCacheSpan( - String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { - super(key, position, length, lastAccessTimestamp, file); + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { + super(key, position, length, lastTouchTimestamp, file); } /** - * Returns a copy of this CacheSpan with a new file and last access timestamp. + * Returns a copy of this CacheSpan with a new file and last touch timestamp. * * @param file The new file. - * @param lastAccessTimestamp The new last access time. - * @return A copy with the new file and last access timestamp. + * @param lastTouchTimestamp The new last touch time. + * @return A copy with the new file and last touch timestamp. * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). */ - public SimpleCacheSpan copyWithFileAndLastAccessTimestamp(File file, long lastAccessTimestamp) { + public SimpleCacheSpan copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) { Assertions.checkState(isCached); - return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index bebcf0ec124..cee5703ff88 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -108,7 +108,7 @@ public void testAddGetRemove() throws Exception { cachedContent1.id, /* offset= */ 10, cacheFileLength, - /* lastAccessTimestamp= */ 30); + /* lastTouchTimestamp= */ 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index); assertThat(span).isNotNull(); cachedContent1.addSpan(span); @@ -293,7 +293,7 @@ public void testCantRemoveNotEmptyCachedContent() throws Exception { cachedContent.id, /* offset= */ 10, cacheFileLength, - /* lastAccessTimestamp= */ 30); + /* lastTouchTimestamp= */ 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); cachedContent.addSpan(span); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 5efdf361918..b00ee73f0f2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -134,8 +134,8 @@ private CacheSpan newCacheSpan(int position, int length) throws IOException { } public static File createCacheSpanFile( - File cacheDir, int id, long offset, int length, long lastAccessTimestamp) throws IOException { - File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + File cacheDir, int id, long offset, int length, long lastTouchTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); createTestFile(cacheFile, length); return cacheFile; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 028937dc5a4..39be9fbcd84 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -38,9 +38,8 @@ public class SimpleCacheSpanTest { public static File createCacheSpanFile( - File cacheDir, int id, long offset, long length, long lastAccessTimestamp) - throws IOException { - File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + File cacheDir, int id, long offset, long length, long lastTouchTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); createTestFile(cacheFile, length); return cacheFile; } @@ -117,7 +116,7 @@ public void testUpgradeFileName() throws Exception { SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, file.length(), index); if (cacheSpan != null) { assertThat(cacheSpan.key).isEqualTo(key); - cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp); + cachedPositions.put(cacheSpan.position, cacheSpan.lastTouchTimestamp); } } @@ -140,12 +139,11 @@ private File createTestFile(String name) throws IOException { return file; } - private void assertCacheSpan(String key, long offset, long lastAccessTimestamp) + private void assertCacheSpan(String key, long offset, long lastTouchTimestamp) throws IOException { int id = index.assignIdForKey(key); long cacheFileLength = 1; - File cacheFile = - createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastAccessTimestamp); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastTouchTimestamp); SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); String message = cacheFile.toString(); assertWithMessage(message).that(cacheSpan).isNotNull(); @@ -155,14 +153,13 @@ private void assertCacheSpan(String key, long offset, long lastAccessTimestamp) assertWithMessage(message).that(cacheSpan.length).isEqualTo(1); assertWithMessage(message).that(cacheSpan.isCached).isTrue(); assertWithMessage(message).that(cacheSpan.file).isEqualTo(cacheFile); - assertWithMessage(message).that(cacheSpan.lastAccessTimestamp).isEqualTo(lastAccessTimestamp); + assertWithMessage(message).that(cacheSpan.lastTouchTimestamp).isEqualTo(lastTouchTimestamp); } - private void assertNullCacheSpan(File parent, String key, long offset, - long lastAccessTimestamp) { + private void assertNullCacheSpan(File parent, String key, long offset, long lastTouchTimestamp) { long cacheFileLength = 0; - File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, - lastAccessTimestamp); + File cacheFile = + SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, lastTouchTimestamp); CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); assertWithMessage(cacheFile.toString()).that(cacheSpan).isNull(); } From 289a8ffe4ced9050e8ef3fedd0955c213a3ce99d Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 17:20:18 +0100 Subject: [PATCH 008/219] Small javadoc fix for DownloadManager constructors PiperOrigin-RevId: 244009343 --- .../google/android/exoplayer2/offline/DownloadManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fdb3ca1840b..915f3750275 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 @@ -186,7 +186,7 @@ default void onRequirementsStateChanged( * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param databaseProvider Provides the {@link DownloadIndex} that holds the downloads. + * @param databaseProvider Provides the database that holds the downloads. * @param downloaderFactory A factory for creating {@link Downloader}s. */ public DownloadManager( @@ -204,7 +204,7 @@ public DownloadManager( * Constructs a {@link DownloadManager}. * * @param context Any context. - * @param databaseProvider Provides the {@link DownloadIndex} that holds the downloads. + * @param databaseProvider Provides the database that holds the downloads. * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param minRetryCount The minimum number of times a download must be retried before failing. From 0748566482161e12d18e85751b2fea39fcdad37d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Apr 2019 20:45:03 +0100 Subject: [PATCH 009/219] Remove TODOs we're not going to do 1. customCacheKey for DASH/HLS/SS is now asserted against in DownloadRequest 2. Merging of event delivery in DownloadManager is very tricky to get right and probably not a good idea PiperOrigin-RevId: 244048392 --- .../android/exoplayer2/offline/DefaultDownloaderFactory.java | 1 - .../com/google/android/exoplayer2/offline/DownloadManager.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java index 9a4e5925ee1..ca20c769dcf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -98,7 +98,6 @@ private Downloader createDownloader( throw new IllegalStateException("Module missing for: " + request.type); } try { - // TODO: Support customCacheKey in DASH/HLS/SS, for completeness. return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper); } catch (Exception e) { throw new RuntimeException("Failed to instantiate downloader for: " + request.type, 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 915f3750275..df958f8691e 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 @@ -485,8 +485,6 @@ private boolean handleMainMessage(Message message) { return true; } - // TODO: Merge these three events into a single MSG_STATE_CHANGE that can carry all updates. This - // allows updating idle at the same point as the downloads that can be queried changes. private void onInitialized(List downloads) { initialized = true; this.downloads.addAll(downloads); From 898bfbff6c9cf677e6c4d83205d31557608d98d9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 00:58:44 +0100 Subject: [PATCH 010/219] [libvpx] permalaunch number of buffers. PiperOrigin-RevId: 244094942 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 952e15aad6d..d5da9a011d6 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -221,8 +221,8 @@ public LibvpxVideoRenderer( disableLoopFilter, /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), - /* numInputBuffers= */ 8, - /* numOutputBuffers= */ 8); + /* numInputBuffers= */ 4, + /* numOutputBuffers= */ 4); } /** From c2bbf38ee8798246a0060897712a79154437d392 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 08:35:07 +0100 Subject: [PATCH 011/219] Extend Bluetooth dead audio track workaround to Q PiperOrigin-RevId: 244139959 --- .../android/exoplayer2/audio/AudioTrackPositionTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 2ce9b8bdbe2..e87e49d2da6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -517,7 +517,7 @@ private long getPlaybackHeadPosition() { rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; } - if (Util.SDK_INT <= 28) { + if (Util.SDK_INT <= 29) { if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0 && state == PLAYSTATE_PLAYING) { From be0acc3621759a5c900f19185bfda34931ba55e3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Apr 2019 13:17:04 +0100 Subject: [PATCH 012/219] Add test for HlsTrackMetadataEntry population in the HlsPlaylistParser PiperOrigin-RevId: 244168713 --- .../playlist/HlsMasterPlaylistParserTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 97d330cdaa3..095739271e3 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -23,10 +23,14 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -146,6 +150,50 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n" + "http://example.com/{$tricky}\n"; + private static final String PLAYLIST_WITH_MATCHING_STREAM_INF_URLS = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2227464," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=6453202," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v8/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=5054232," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "v7/prog_index.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2448841," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8399417," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v9/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=5275609," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud2\",SUBTITLES=\"sub1\"\n" + + "v7/prog_index.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2256841," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v5/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8207417," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v9/prog_index.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=6482579," + + "CLOSED-CAPTIONS=\"cc1\",AUDIO=\"aud3\",SUBTITLES=\"sub1\"\n" + + "v8/prog_index.m3u8\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",NAME=\"English\",URI=\"a2/index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud3\",NAME=\"English\",URI=\"a3/index.m3u8\"\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," + + "GROUP-ID=\"cc1\",NAME=\"English\",INSTREAM-ID=\"CC1\"\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=SUBTITLES," + + "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -296,6 +344,61 @@ public void testVariableSubstitution() throws IOException { .isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work")); } + @Test + public void testHlsMetadata() throws IOException { + HlsMasterPlaylist playlist = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); + assertThat(playlist.variants).hasSize(4); + assertThat(playlist.variants.get(0).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 2227464, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 2448841, /* audioGroupId= */ "aud2"), + createVariantInfo(/* bitrate= */ 2256841, /* audioGroupId= */ "aud3"))); + assertThat(playlist.variants.get(1).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 6453202, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 6482579, /* audioGroupId= */ "aud3"))); + assertThat(playlist.variants.get(2).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 5054232, /* audioGroupId= */ "aud1"), + createVariantInfo(/* bitrate= */ 5275609, /* audioGroupId= */ "aud2"))); + assertThat(playlist.variants.get(3).format.metadata) + .isEqualTo( + createExtXStreamInfMetadata( + createVariantInfo(/* bitrate= */ 8399417, /* audioGroupId= */ "aud2"), + createVariantInfo(/* bitrate= */ 8207417, /* audioGroupId= */ "aud3"))); + + assertThat(playlist.audios).hasSize(3); + assertThat(playlist.audios.get(0).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud1", /* name= */ "English")); + assertThat(playlist.audios.get(1).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud2", /* name= */ "English")); + assertThat(playlist.audios.get(2).format.metadata) + .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English")); + } + + private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { + return new Metadata( + new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); + } + + private static Metadata createExtXMediaMetadata(String groupId, String name) { + return new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList())); + } + + private static HlsTrackMetadataEntry.VariantInfo createVariantInfo( + long bitrate, String audioGroupId) { + return new HlsTrackMetadataEntry.VariantInfo( + bitrate, + /* videoGroupId= */ null, + audioGroupId, + /* subtitleGroupId= */ "sub1", + /* captionGroupId= */ "cc1"); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); From a501f8c2452eafafb4c792179fc342a71e3bcc03 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 18 Apr 2019 13:34:52 +0100 Subject: [PATCH 013/219] Fix flaky DownloadManagerDashTest PiperOrigin-RevId: 244170179 --- .../offline/DownloadManagerTest.java | 2 +- .../dash/offline/DownloadManagerDashTest.java | 41 +++++++++++-------- .../testutil/TestDownloadManagerListener.java | 9 +++- 3 files changed, 34 insertions(+), 18 deletions(-) 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 f23248952ce..140347bd910 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 @@ -552,7 +552,7 @@ private void releaseDownloadManager() throws Exception { } } - private void runOnMainThread(final TestRunnable r) { + private void runOnMainThread(TestRunnable r) { dummyMainThread.runTestOnMainThread(r); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 76356cf3a85..0dce24bf1d4 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; +import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.RobolectricUtil; @@ -100,8 +101,8 @@ public void setUp() throws Exception { } @After - public void tearDown() throws Exception { - downloadManager.release(); + public void tearDown() { + runOnMainThread(() -> downloadManager.release()); Util.recursiveDelete(tempFolder); dummyMainThread.release(); } @@ -129,10 +130,11 @@ public void testSaveAndLoadActionFile() throws Throwable { // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded // actions. - dummyMainThread.runOnMainThread( + runOnMainThread( () -> { // Setup an Action and immediately release the DM. - handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); + DownloadRequest request = getDownloadRequest(fakeStreamKey1, fakeStreamKey2); + downloadManager.addDownload(request); downloadManager.release(); }); @@ -229,25 +231,28 @@ private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable } private void handleDownloadRequest(StreamKey... keys) { + DownloadRequest request = getDownloadRequest(keys); + runOnMainThread(() -> downloadManager.addDownload(request)); + } + + private DownloadRequest getDownloadRequest(StreamKey... keys) { ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - DownloadRequest action = - new DownloadRequest( - TEST_ID, - DownloadRequest.TYPE_DASH, - TEST_MPD_URI, - keysList, - /* customCacheKey= */ null, - null); - downloadManager.addDownload(action); + return new DownloadRequest( + TEST_ID, + DownloadRequest.TYPE_DASH, + TEST_MPD_URI, + keysList, + /* customCacheKey= */ null, + null); } private void handleRemoveAction() { - downloadManager.removeDownload(TEST_ID); + runOnMainThread(() -> downloadManager.removeDownload(TEST_ID)); } private void createDownloadManager() { - dummyMainThread.runTestOnMainThread( + runOnMainThread( () -> { Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); downloadManager = @@ -261,9 +266,13 @@ private void createDownloadManager() { new Requirements(0)); downloadManagerListener = - new TestDownloadManagerListener(downloadManager, dummyMainThread); + new TestDownloadManagerListener( + downloadManager, dummyMainThread, /* timeout= */ 3000); downloadManager.startDownloads(); }); } + private void runOnMainThread(TestRunnable r) { + dummyMainThread.runTestOnMainThread(r); + } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index b74e539fd66..9d6223b8b11 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -40,14 +40,21 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen private final DummyMainThread dummyMainThread; private final HashMap> downloadStates; private final ConditionVariable initializedCondition; + private final int timeout; private CountDownLatch downloadFinishedCondition; @Download.FailureReason private int failureReason; public TestDownloadManagerListener( DownloadManager downloadManager, DummyMainThread dummyMainThread) { + this(downloadManager, dummyMainThread, TIMEOUT); + } + + public TestDownloadManagerListener( + DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeout) { this.downloadManager = downloadManager; this.dummyMainThread = dummyMainThread; + this.timeout = timeout; downloadStates = new HashMap<>(); initializedCondition = new ConditionVariable(); downloadManager.addListener(this); @@ -110,7 +117,7 @@ public void blockUntilTasksComplete() throws InterruptedException { downloadFinishedCondition.countDown(); } }); - assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(downloadFinishedCondition.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); } private ArrayBlockingQueue getStateQueue(String taskId) { From b6337adc4724236ef6b1d033b01feeb563437777 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Apr 2019 14:41:45 +0100 Subject: [PATCH 014/219] Avoid selecting a forced text track that doesn't match the audio selection Assuming there is no text language preference. PiperOrigin-RevId: 244176667 --- RELEASENOTES.md | 4 +- .../trackselection/DefaultTrackSelector.java | 40 +++++++++---------- .../DefaultTrackSelectorTest.java | 36 +++++++---------- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 182701ec34c..015b348f687 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,7 +41,7 @@ * MP3: Fix ID3 frame unsychronization ([#5673](https://github.com/google/ExoPlayer/issues/5673)). * MP3: Fix playback of badly clipped files - ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default (i.e. if the flag is not set), the 0x82 elementary stream type is now treated as an SCTE subtitle track @@ -52,6 +52,8 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Allow to specify a selection reason for a `SelectionOverride`. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. * UI: * Update `DefaultTimeBar` based on duration of media and add parameter to set the minimum update interval to control the smoothness of the updates diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f25f1a979cc..3200e404954 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -2070,29 +2070,25 @@ protected Pair selectTextTrack( boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage); - if (languageScore > 0 - || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { + boolean trackHasNoLanguage = formatHasNoLanguage(format); + if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) { if (isDefault) { - trackScore = 17; + trackScore = 11; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 13; + trackScore = 7; } else { - trackScore = 9; + trackScore = 3; } trackScore += languageScore; } else if (isDefault) { - trackScore = 8; - } else if (isForced) { - int preferredAudioLanguageScore = - getFormatLanguageScore(format, params.preferredAudioLanguage); - if (preferredAudioLanguageScore > 0) { - trackScore = 4 + preferredAudioLanguageScore; - } else { - trackScore = 1 + getFormatLanguageScore(format, selectedAudioLanguage); - } + trackScore = 2; + } else if (isForced + && (getFormatLanguageScore(format, selectedAudioLanguage) > 0 + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { + trackScore = 1; } else { // Track should not be selected. continue; @@ -2281,15 +2277,19 @@ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapa && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */ + protected static boolean formatHasNoLanguage(Format format) { + return stringDefinesNoLanguage(format.language); + } + /** - * Returns whether a {@link Format} does not define a language. + * Returns whether the given string does not define a language. * - * @param format The {@link Format}. - * @return Whether the {@link Format} does not define a language. + * @param language The string. + * @return Whether the given string does not define a language. */ - protected static boolean formatHasNoLanguage(Format format) { - return TextUtils.isEmpty(format.language) - || TextUtils.equals(format.language, C.LANGUAGE_UNDETERMINED); + protected static boolean stringDefinesNoLanguage(@Nullable String language) { + return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 3091e46456f..83fe34db972 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -910,13 +910,8 @@ public void testTextTrackSelectionFlags() throws ExoPlaybackException { result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); - // With no language preference and no text track flagged as default, the first forced should be + // Default flags are disabled and no language preference is provided, so no text track is // selected. - trackGroups = wrapFormats(forcedOnly, noFlag); - result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnly); - - // Default flags are disabled, so the first track flagged as forced should be selected. trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault); trackSelector.setParameters( Parameters.DEFAULT @@ -924,15 +919,7 @@ public void testTextTrackSelectionFlags() throws ExoPlaybackException { .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) .build()); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnly); - - // Default flags are disabled, but there is a text track flagged as forced whose language - // matches the preferred audio language. - trackGroups = wrapFormats(forcedDefault, forcedOnly, defaultOnly, noFlag, forcedOnlySpanish); - trackSelector.setParameters( - trackSelector.getParameters().buildUpon().setPreferredTextLanguage("spa").build()); - result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedOnlySpanish); + assertNoSelection(result.selections.get(0)); // All selection flags are disabled and there is no language preference, so nothing should be // selected. @@ -977,6 +964,11 @@ public void testSelectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybac buildTextFormat(/* id= */ "forcedEnglish", /* language= */ "eng", C.SELECTION_FLAG_FORCED); Format forcedGerman = buildTextFormat(/* id= */ "forcedGerman", /* language= */ "deu", C.SELECTION_FLAG_FORCED); + Format forcedNoLanguage = + buildTextFormat( + /* id= */ "forcedNoLanguage", + /* language= */ C.LANGUAGE_UNDETERMINED, + C.SELECTION_FLAG_FORCED); Format audio = buildAudioFormat(/* id= */ "audio"); Format germanAudio = buildAudioFormat( @@ -994,16 +986,18 @@ public void testSelectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybac ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES }; - // The audio declares no language. The first forced text track should be selected. - TrackGroupArray trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman); + // Neither the audio nor the forced text track define a language. We select them both under the + // assumption that they have matching language. + TrackGroupArray trackGroups = wrapFormats(audio, forcedNoLanguage); TrackSelectorResult result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(1), trackGroups, forcedEnglish); + assertFixedSelection(result.selections.get(1), trackGroups, forcedNoLanguage); - // Ditto. - trackGroups = wrapFormats(audio, forcedGerman, forcedEnglish); + // No forced text track should be selected because none of the forced text tracks' languages + // matches the selected audio language. + trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman); + assertNoSelection(result.selections.get(1)); // The audio declares german. The german forced track should be selected. trackGroups = wrapFormats(germanAudio, forcedGerman, forcedEnglish); From 6d8bd34590f88110041e3ff6ee085b3235b5eaaa Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 18 Apr 2019 17:04:31 +0100 Subject: [PATCH 015/219] Add missing DownloadService build*Intent and startWith* methods PiperOrigin-RevId: 244196081 --- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- .../exoplayer2/demo/DownloadTracker.java | 4 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 2 +- .../exoplayer2/offline/DownloadService.java | 127 ++++++++++++++---- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- testutils_robolectric/build.gradle | 2 +- 21 files changed, 123 insertions(+), 46 deletions(-) diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 1d2068e5f75..33161b4121c 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 84a8a4087cf..7089d4d7314 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,7 +62,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' 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 4a7a810314a..a860d96e43b 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 @@ -101,7 +101,7 @@ public void toggleDownload( RenderersFactory renderersFactory) { Download download = downloads.get(uri); if (download != null) { - DownloadService.startWithRemoveDownload( + DownloadService.sendRemoveDownload( context, DemoDownloadService.class, download.request.id, /* foreground= */ false); } else { if (startDownloadDialogHelper != null) { @@ -263,7 +263,7 @@ private void startDownload() { } private void startDownload(DownloadRequest downloadRequest) { - DownloadService.startWithNewDownload( + DownloadService.sendNewDownload( context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); } diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 573426df2e6..4dc463ff81c 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:16.1.2' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index baf925acbdd..ad45f61d989 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { api 'org.chromium.net:cronet-embedded:72.3626.96' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ee3358d21ad..ffecdcd16f1 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 9a247c3f8fd..06a58884046 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 6c4bfa469a4..50acd6c0404 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index a86eedc2d43..c6f5a216ce3 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index eddd3643706..db2e073c8ae 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index e7c7fce1644..ca734c36572 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 4b2ba26ca2c..02b68b831d6 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index deb9f24dcec..68ff8cc9771 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion 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 c206a94d6d5..6922d6a7872 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 @@ -117,8 +117,8 @@ public abstract class DownloadService extends Service { public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_START}, {@link #ACTION_STOP} and {@link - * #ACTION_REMOVE} intents. + * Key for the content id in {@link #ACTION_SET_MANUAL_STOP_REASON} and {@link #ACTION_REMOVE} + * intents. */ public static final String KEY_CONTENT_ID = "content_id"; @@ -265,10 +265,9 @@ public static Intent buildAddRequestIntent( DownloadRequest downloadRequest, int manualStopReason, boolean foreground) { - return getIntent(context, clazz, ACTION_ADD) + return getIntent(context, clazz, ACTION_ADD, foreground) .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) - .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason) - .putExtra(KEY_FOREGROUND, foreground); + .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); } /** @@ -282,9 +281,7 @@ public static Intent buildAddRequestIntent( */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { - return getIntent(context, clazz, ACTION_REMOVE) - .putExtra(KEY_CONTENT_ID, id) - .putExtra(KEY_FOREGROUND, foreground); + return getIntent(context, clazz, ACTION_REMOVE, foreground).putExtra(KEY_CONTENT_ID, id); } /** @@ -295,55 +292,122 @@ public static Intent buildRemoveDownloadIntent( * @param clazz The concrete download service being targeted by the intent. * @param id The content id, or {@code null} to set the manual stop reason for all downloads. * @param manualStopReason An application defined stop reason. + * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ public static Intent buildSetManualStopReasonIntent( Context context, Class clazz, @Nullable String id, - int manualStopReason) { - return getIntent(context, clazz, ACTION_STOP) + int manualStopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_MANUAL_STOP_REASON, foreground) .putExtra(KEY_CONTENT_ID, id) .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); } /** - * Starts the service, adding a new download. + * Builds an {@link Intent} for starting 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 Created Intent. + */ + public static Intent buildStartDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_START, foreground); + } + + /** + * Builds an {@link Intent} for stopping 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 Created Intent. + */ + public static Intent buildStopDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_STOP, foreground); + } + + /** + * Starts the service if not started already and adds a new download. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. * @param downloadRequest The request to be executed. * @param foreground Whether the service is started in the foreground. */ - public static void startWithNewDownload( + public static void sendNewDownload( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, foreground); - if (foreground) { - Util.startForegroundService(context, intent); - } else { - context.startService(intent); - } + startService(context, intent, foreground); } /** - * Starts the service to remove a download. + * Starts the service if not started already and removes a download. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. * @param id The content id. * @param foreground Whether the service is started in the foreground. */ - public static void startWithRemoveDownload( + public static void sendRemoveDownload( Context context, Class clazz, String id, boolean foreground) { Intent intent = buildRemoveDownloadIntent(context, clazz, id, foreground); - if (foreground) { - Util.startForegroundService(context, intent); - } else { - context.startService(intent); - } + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the manual stop reason for one or all + * downloads. To clear manual stop reason, pass {@link Download#MANUAL_STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id, or {@code null} to set the manual stop reason for all downloads. + * @param manualStopReason An application defined stop reason. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendManualStopReason( + Context context, + Class clazz, + @Nullable String id, + int manualStopReason, + boolean foreground) { + Intent intent = + buildSetManualStopReasonIntent(context, clazz, id, manualStopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and starts 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 sendStartDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildStartDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and stops 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 sendStopDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildStopDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); } /** @@ -367,7 +431,7 @@ public static void start(Context context, Class clazz * @see #start(Context, Class) */ public static void startForeground(Context context, Class clazz) { - Intent intent = getIntent(context, clazz, ACTION_INIT).putExtra(KEY_FOREGROUND, true); + Intent intent = getIntent(context, clazz, ACTION_INIT, true); Util.startForegroundService(context, intent); } @@ -588,11 +652,24 @@ private void logd(String message) { } } + private static Intent getIntent( + Context context, Class clazz, String action, boolean foreground) { + return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); + } + private static Intent getIntent( Context context, Class clazz, String action) { return new Intent(context, clazz).setAction(action); } + private static void startService(Context context, Intent intent, boolean foreground) { + if (foreground) { + Util.startForegroundService(context, intent); + } else { + context.startService(intent); + } + } + private final class ForegroundNotificationUpdater { private final int notificationId; diff --git a/library/dash/build.gradle b/library/dash/build.gradle index c7e68f548a0..f6981a22204 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 99619bf7500..8e9696af70f 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index ba3b4ab65db..a2e81fb3041 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 9c47f3684dc..49446b25de5 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.0' - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 0e1c8a1268b..dd5cfa64a78 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.1' + androidTestImplementation 'androidx.annotation:annotation:1.0.2' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index ab78e6673f1..bdc26d5c195 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'androidx.test.ext:truth:' + androidXTestVersion - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index 44459ea272c..a3859a9e489 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,5 +41,5 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.1' + implementation 'androidx.annotation:annotation:1.0.2' } From 138da6d51900e831ee4bcaee885bb373655d7b90 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 18:26:00 +0100 Subject: [PATCH 016/219] Rename manualStopReason to stopReason PiperOrigin-RevId: 244210737 --- .../offline/ActionFileUpgradeUtil.java | 4 +- .../offline/DefaultDownloadIndex.java | 20 ++-- .../android/exoplayer2/offline/Download.java | 20 ++-- .../exoplayer2/offline/DownloadManager.java | 93 +++++++++---------- .../exoplayer2/offline/DownloadService.java | 89 ++++++++---------- .../offline/WritableDownloadIndex.java | 16 ++-- .../offline/DefaultDownloadIndexTest.java | 47 +++++----- .../exoplayer2/offline/DownloadBuilder.java | 8 +- .../offline/DownloadManagerTest.java | 28 +++--- 9 files changed, 153 insertions(+), 172 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 0a37fe3a808..51996ed2843 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -90,7 +90,7 @@ public static void upgradeAndDelete( DownloadRequest request, DefaultDownloadIndex downloadIndex) throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { - download = DownloadManager.mergeRequest(download, request, download.manualStopReason); + download = DownloadManager.mergeRequest(download, request, download.stopReason); } else { long nowMs = System.currentTimeMillis(); download = @@ -98,7 +98,7 @@ public static void upgradeAndDelete( request, STATE_QUEUED, Download.FAILURE_REASON_NONE, - Download.MANUAL_STOP_REASON_NONE, + Download.STOP_REASON_NONE, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs); } 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 fc1518e5c32..a2caff3ff11 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 @@ -57,7 +57,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes"; private static final String COLUMN_TOTAL_BYTES = "total_bytes"; private static final String COLUMN_FAILURE_REASON = "failure_reason"; - private static final String COLUMN_MANUAL_STOP_REASON = "manual_stop_reason"; + private static final String COLUMN_STOP_REASON = "manual_stop_reason"; private static final String COLUMN_START_TIME_MS = "start_time_ms"; private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; @@ -82,7 +82,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 8; private static final int COLUMN_INDEX_TOTAL_BYTES = 9; private static final int COLUMN_INDEX_FAILURE_REASON = 10; - private static final int COLUMN_INDEX_MANUAL_STOP_REASON = 11; + private static final int COLUMN_INDEX_STOP_REASON = 11; private static final int COLUMN_INDEX_START_TIME_MS = 12; private static final int COLUMN_INDEX_UPDATE_TIME_MS = 13; @@ -103,7 +103,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { COLUMN_DOWNLOADED_BYTES, COLUMN_TOTAL_BYTES, COLUMN_FAILURE_REASON, - COLUMN_MANUAL_STOP_REASON, + COLUMN_STOP_REASON, COLUMN_START_TIME_MS, COLUMN_UPDATE_TIME_MS }; @@ -135,7 +135,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { + " INTEGER NOT NULL," + COLUMN_NOT_MET_REQUIREMENTS + " INTEGER NOT NULL," - + COLUMN_MANUAL_STOP_REASON + + COLUMN_STOP_REASON + " INTEGER NOT NULL," + COLUMN_START_TIME_MS + " INTEGER NOT NULL," @@ -202,7 +202,7 @@ public void putDownload(Download download) throws DatabaseIOException { values.put(COLUMN_FAILURE_REASON, download.failureReason); values.put(COLUMN_STOP_FLAGS, 0); values.put(COLUMN_NOT_MET_REQUIREMENTS, 0); - values.put(COLUMN_MANUAL_STOP_REASON, download.manualStopReason); + values.put(COLUMN_STOP_REASON, download.stopReason); values.put(COLUMN_START_TIME_MS, download.startTimeMs); values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); try { @@ -224,11 +224,11 @@ public void removeDownload(String id) throws DatabaseIOException { } @Override - public void setManualStopReason(int manualStopReason) throws DatabaseIOException { + public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); try { ContentValues values = new ContentValues(); - values.put(COLUMN_MANUAL_STOP_REASON, manualStopReason); + values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update(TABLE_NAME, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); } catch (SQLException e) { @@ -237,11 +237,11 @@ public void setManualStopReason(int manualStopReason) throws DatabaseIOException } @Override - public void setManualStopReason(String id, int manualStopReason) throws DatabaseIOException { + public void setStopReason(String id, int stopReason) throws DatabaseIOException { ensureInitialized(); try { ContentValues values = new ContentValues(); - values.put(COLUMN_MANUAL_STOP_REASON, manualStopReason); + values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update( TABLE_NAME, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); @@ -332,7 +332,7 @@ private static Download getDownloadForCurrentRow(Cursor cursor) { request, cursor.getInt(COLUMN_INDEX_STATE), cursor.getInt(COLUMN_INDEX_FAILURE_REASON), - cursor.getInt(COLUMN_INDEX_MANUAL_STOP_REASON), + cursor.getInt(COLUMN_INDEX_STOP_REASON), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), cachingCounters); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index b29abde24b2..343b9d6a496 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -46,7 +46,7 @@ public final class Download { // Important: These constants are persisted into DownloadIndex. Do not change them. /** The download is waiting to be started. */ public static final int STATE_QUEUED = 0; - /** The download is stopped for a specified {@link #manualStopReason}. */ + /** The download is stopped for a specified {@link #stopReason}. */ public static final int STATE_STOPPED = 1; /** The download is currently started. */ public static final int STATE_DOWNLOADING = 2; @@ -69,8 +69,8 @@ public final class Download { /** The download is failed because of unknown reason. */ public static final int FAILURE_REASON_UNKNOWN = 1; - /** The download isn't manually stopped. */ - public static final int MANUAL_STOP_REASON_NONE = 0; + /** The download isn't stopped. */ + public static final int STOP_REASON_NONE = 0; /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { @@ -108,8 +108,8 @@ public static String getStateString(@State int state) { * #FAILURE_REASON_NONE}. */ @FailureReason public final int failureReason; - /** The reason the download is manually stopped, or {@link #MANUAL_STOP_REASON_NONE}. */ - public final int manualStopReason; + /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ + public final int stopReason; /* package */ CachingCounters counters; @@ -117,14 +117,14 @@ public static String getStateString(@State int state) { DownloadRequest request, @State int state, @FailureReason int failureReason, - int manualStopReason, + int stopReason, long startTimeMs, long updateTimeMs) { this( request, state, failureReason, - manualStopReason, + stopReason, startTimeMs, updateTimeMs, new CachingCounters()); @@ -134,19 +134,19 @@ public static String getStateString(@State int state) { DownloadRequest request, @State int state, @FailureReason int failureReason, - int manualStopReason, + int stopReason, long startTimeMs, long updateTimeMs, CachingCounters counters) { Assertions.checkNotNull(counters); Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); - if (manualStopReason != 0) { + if (stopReason != 0) { Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; this.failureReason = failureReason; - this.manualStopReason = manualStopReason; + this.stopReason = stopReason; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; this.counters = counters; 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 df958f8691e..497e3476afc 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 @@ -17,7 +17,6 @@ import static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_NONE; import static com.google.android.exoplayer2.offline.Download.FAILURE_REASON_UNKNOWN; -import static com.google.android.exoplayer2.offline.Download.MANUAL_STOP_REASON_NONE; import static com.google.android.exoplayer2.offline.Download.STATE_COMPLETED; import static com.google.android.exoplayer2.offline.Download.STATE_DOWNLOADING; import static com.google.android.exoplayer2.offline.Download.STATE_FAILED; @@ -25,6 +24,7 @@ import static com.google.android.exoplayer2.offline.Download.STATE_REMOVING; import static com.google.android.exoplayer2.offline.Download.STATE_RESTARTING; import static com.google.android.exoplayer2.offline.Download.STATE_STOPPED; +import static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; import android.content.Context; import android.os.Handler; @@ -128,7 +128,7 @@ default void onRequirementsStateChanged( private static final int MSG_INITIALIZE = 0; private static final int MSG_SET_DOWNLOADS_STARTED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; - private static final int MSG_SET_MANUAL_STOP_REASON = 3; + private static final int MSG_SET_STOP_REASON = 3; private static final int MSG_ADD_DOWNLOAD = 4; private static final int MSG_REMOVE_DOWNLOAD = 5; private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; @@ -346,10 +346,7 @@ public List getCurrentDownloads() { return Collections.unmodifiableList(new ArrayList<>(downloads)); } - /** - * Starts all downloads except those that are manually stopped (i.e. have a non-zero {@link - * Download#manualStopReason}). - */ + /** Starts all downloads except those that have a non-zero {@link Download#stopReason}. */ public void startDownloads() { pendingMessages++; internalHandler @@ -366,17 +363,17 @@ public void stopDownloads() { } /** - * Sets the manual stop reason for one or all downloads. To clear the manual stop reason, pass - * {@link Download#MANUAL_STOP_REASON_NONE}. + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. * - * @param id The content id of the download to update, or {@code null} to set the manual stop - * reason for all downloads. - * @param manualStopReason The manual stop reason, or {@link Download#MANUAL_STOP_REASON_NONE}. + * @param id The content id of the download to update, or {@code null} to set the stop reason for + * all downloads. + * @param stopReason The stop reason, or {@link Download#STOP_REASON_NONE}. */ - public void setManualStopReason(@Nullable String id, int manualStopReason) { + public void setStopReason(@Nullable String id, int stopReason) { pendingMessages++; internalHandler - .obtainMessage(MSG_SET_MANUAL_STOP_REASON, manualStopReason, /* unused */ 0, id) + .obtainMessage(MSG_SET_STOP_REASON, stopReason, /* unused */ 0, id) .sendToTarget(); } @@ -386,20 +383,20 @@ public void setManualStopReason(@Nullable String id, int manualStopReason) { * @param request The download request. */ public void addDownload(DownloadRequest request) { - addDownload(request, Download.MANUAL_STOP_REASON_NONE); + addDownload(request, Download.STOP_REASON_NONE); } /** - * Adds a download defined by the given request and with the specified manual stop reason. + * Adds a download defined by the given request and with the specified stop reason. * * @param request The download request. - * @param manualStopReason An initial manual stop reason for the download, or {@link - * Download#MANUAL_STOP_REASON_NONE} if the download should be started. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. */ - public void addDownload(DownloadRequest request, int manualStopReason) { + public void addDownload(DownloadRequest request, int stopReason) { pendingMessages++; internalHandler - .obtainMessage(MSG_ADD_DOWNLOAD, manualStopReason, /* unused */ 0, request) + .obtainMessage(MSG_ADD_DOWNLOAD, stopReason, /* unused */ 0, request) .sendToTarget(); } @@ -552,15 +549,15 @@ private boolean handleInternalMessage(Message message) { notMetRequirements = message.arg1; setNotMetRequirementsInternal(notMetRequirements); break; - case MSG_SET_MANUAL_STOP_REASON: + case MSG_SET_STOP_REASON: String id = (String) message.obj; - int manualStopReason = message.arg1; - setManualStopReasonInternal(id, manualStopReason); + int stopReason = message.arg1; + setStopReasonInternal(id, stopReason); break; case MSG_ADD_DOWNLOAD: DownloadRequest request = (DownloadRequest) message.obj; - manualStopReason = message.arg1; - addDownloadInternal(request, manualStopReason); + stopReason = message.arg1; + addDownloadInternal(request, stopReason); break; case MSG_REMOVE_DOWNLOAD: id = (String) message.obj; @@ -629,34 +626,34 @@ private void setNotMetRequirementsInternal( } } - private void setManualStopReasonInternal(@Nullable String id, int manualStopReason) { + private void setStopReasonInternal(@Nullable String id, int stopReason) { if (id != null) { DownloadInternal downloadInternal = getDownload(id); if (downloadInternal != null) { - logd("download manual stop reason is set to : " + manualStopReason, downloadInternal); - downloadInternal.setManualStopReason(manualStopReason); + logd("download stop reason is set to : " + stopReason, downloadInternal); + downloadInternal.setStopReason(stopReason); return; } } else { for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setManualStopReason(manualStopReason); + downloadInternals.get(i).setStopReason(stopReason); } } try { if (id != null) { - downloadIndex.setManualStopReason(id, manualStopReason); + downloadIndex.setStopReason(id, stopReason); } else { - downloadIndex.setManualStopReason(manualStopReason); + downloadIndex.setStopReason(stopReason); } } catch (IOException e) { - Log.e(TAG, "setManualStopReason failed", e); + Log.e(TAG, "setStopReason failed", e); } } - private void addDownloadInternal(DownloadRequest request, int manualStopReason) { + private void addDownloadInternal(DownloadRequest request, int stopReason) { DownloadInternal downloadInternal = getDownload(request.id); if (downloadInternal != null) { - downloadInternal.addRequest(request, manualStopReason); + downloadInternal.addRequest(request, stopReason); logd("Request is added to existing download", downloadInternal); } else { Download download = loadDownload(request.id); @@ -665,14 +662,14 @@ private void addDownloadInternal(DownloadRequest request, int manualStopReason) download = new Download( request, - manualStopReason != Download.MANUAL_STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, Download.FAILURE_REASON_NONE, - manualStopReason, + stopReason, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs); logd("Download state is created for " + request.id); } else { - download = mergeRequest(download, request, manualStopReason); + download = mergeRequest(download, request, stopReason); logd("Download state is loaded for " + request.id); } addDownloadForState(download); @@ -820,11 +817,11 @@ private boolean canStartDownloads() { } /* package */ static Download mergeRequest( - Download download, DownloadRequest request, int manualStopReason) { + Download download, DownloadRequest request, int stopReason) { @Download.State int state = download.state; if (state == STATE_REMOVING || state == STATE_RESTARTING) { state = STATE_RESTARTING; - } else if (manualStopReason != MANUAL_STOP_REASON_NONE) { + } else if (stopReason != STOP_REASON_NONE) { state = STATE_STOPPED; } else { state = STATE_QUEUED; @@ -835,7 +832,7 @@ private boolean canStartDownloads() { download.request.copyWithMergedRequest(request), state, FAILURE_REASON_NONE, - manualStopReason, + stopReason, startTimeMs, /* updateTimeMs= */ nowMs, download.counters); @@ -846,7 +843,7 @@ private static Download copyWithState(Download download, @Download.State int sta download.request, state, FAILURE_REASON_NONE, - download.manualStopReason, + download.stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -882,21 +879,21 @@ private static final class DownloadInternal { // TODO: Get rid of these and use download directly. @Download.State private int state; - private int manualStopReason; + private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; private DownloadInternal(DownloadManager downloadManager, Download download) { this.downloadManager = downloadManager; this.download = download; - manualStopReason = download.manualStopReason; + stopReason = download.stopReason; } private void initialize() { initialize(download.state); } - public void addRequest(DownloadRequest newRequest, int manualStopReason) { - download = mergeRequest(download, newRequest, manualStopReason); + public void addRequest(DownloadRequest newRequest, int stopReason) { + download = mergeRequest(download, newRequest, stopReason); initialize(); } @@ -910,7 +907,7 @@ public Download getUpdatedDownload() { download.request, state, state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - manualStopReason, + stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -934,8 +931,8 @@ public void start() { } } - public void setManualStopReason(int manualStopReason) { - this.manualStopReason = manualStopReason; + public void setStopReason(int stopReason) { + this.stopReason = stopReason; updateStopState(); } @@ -981,7 +978,7 @@ private void initialize(int initialState) { } private boolean canStart() { - return downloadManager.canStartDownloads() && manualStopReason == MANUAL_STOP_REASON_NONE; + return downloadManager.canStartDownloads() && stopReason == STOP_REASON_NONE; } private void startOrQueue() { 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 6922d6a7872..fa74afacb3f 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 @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.offline; -import static com.google.android.exoplayer2.offline.Download.MANUAL_STOP_REASON_NONE; +import static com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; import android.app.Notification; import android.app.Service; @@ -58,16 +58,15 @@ public abstract class DownloadService extends Service { *

    *
  • {@link #KEY_DOWNLOAD_REQUEST} - A {@link DownloadRequest} defining the download to be * added. - *
  • {@link #KEY_MANUAL_STOP_REASON} - An initial manual stop reason for the download. If - * omitted {@link Download#MANUAL_STOP_REASON_NONE} is used. + *
  • {@link #KEY_STOP_REASON} - An initial stop reason for the download. If omitted {@link + * Download#STOP_REASON_NONE} is used. *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
*/ public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; /** - * Starts all downloads except those that are manually stopped (i.e. have a non-zero {@link - * Download#manualStopReason}). Extras: + * Starts all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * *
    *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. @@ -87,19 +86,18 @@ public abstract class DownloadService extends Service { "com.google.android.exoplayer.downloadService.action.STOP"; /** - * Sets the manual stop reason for one or all downloads. To clear the manual stop reason, pass - * {@link Download#MANUAL_STOP_REASON_NONE}. Extras: + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. Extras: * *
      *
    • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the manual * stop reason. If omitted, all downloads will be updated. - *
    • {@link #KEY_MANUAL_STOP_REASON} - An application provided reason for stopping the - * download or downloads, or {@link Download#MANUAL_STOP_REASON_NONE} to clear the manual - * stop reason. + *
    • {@link #KEY_STOP_REASON} - An application provided reason for stopping the download or + * downloads, or {@link Download#STOP_REASON_NONE} to clear the manual stop reason. *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ - public static final String ACTION_SET_MANUAL_STOP_REASON = + public static final String ACTION_SET_STOP_REASON = "com.google.android.exoplayer.downloadService.action.SET_MANUAL_STOP_REASON"; /** @@ -117,16 +115,12 @@ public abstract class DownloadService extends Service { public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_SET_MANUAL_STOP_REASON} and {@link #ACTION_REMOVE} - * intents. + * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE} intents. */ public static final String KEY_CONTENT_ID = "content_id"; - /** - * Key for the manual stop reason in {@link #ACTION_SET_MANUAL_STOP_REASON} and {@link - * #ACTION_ADD} intents. - */ - public static final String KEY_MANUAL_STOP_REASON = "manual_stop_reason"; + /** Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD} intents. */ + public static final String KEY_STOP_REASON = "manual_stop_reason"; /** * Key for a boolean extra that can be set on any intent to indicate whether the service was @@ -244,8 +238,7 @@ public static Intent buildAddRequestIntent( Class clazz, DownloadRequest downloadRequest, boolean foreground) { - return buildAddRequestIntent( - context, clazz, downloadRequest, MANUAL_STOP_REASON_NONE, foreground); + return buildAddRequestIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); } /** @@ -254,8 +247,8 @@ public static Intent buildAddRequestIntent( * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. * @param downloadRequest The request to be executed. - * @param manualStopReason An initial manual stop reason for the download, or {@link - * Download#MANUAL_STOP_REASON_NONE} if the download should be started. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ @@ -263,11 +256,11 @@ public static Intent buildAddRequestIntent( Context context, Class clazz, DownloadRequest downloadRequest, - int manualStopReason, + int stopReason, boolean foreground) { return getIntent(context, clazz, ACTION_ADD, foreground) .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) - .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); + .putExtra(KEY_STOP_REASON, stopReason); } /** @@ -285,25 +278,25 @@ public static Intent buildRemoveDownloadIntent( } /** - * Builds an {@link Intent} for setting the manual stop reason for one or all downloads. To clear - * the manual stop reason, pass {@link Download#MANUAL_STOP_REASON_NONE}. + * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the + * stop reason, pass {@link Download#STOP_REASON_NONE}. * * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. - * @param id The content id, or {@code null} to set the manual stop reason for all downloads. - * @param manualStopReason An application defined stop reason. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildSetManualStopReasonIntent( + public static Intent buildSetStopReasonIntent( Context context, Class clazz, @Nullable String id, - int manualStopReason, + int stopReason, boolean foreground) { - return getIntent(context, clazz, ACTION_SET_MANUAL_STOP_REASON, foreground) + return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground) .putExtra(KEY_CONTENT_ID, id) - .putExtra(KEY_MANUAL_STOP_REASON, manualStopReason); + .putExtra(KEY_STOP_REASON, stopReason); } /** @@ -364,23 +357,22 @@ public static void sendRemoveDownload( } /** - * Starts the service if not started already and sets the manual stop reason for one or all - * downloads. To clear manual stop reason, pass {@link Download#MANUAL_STOP_REASON_NONE}. + * Starts the service if not started already and sets the stop reason for one or all downloads. To + * clear stop reason, pass {@link Download#STOP_REASON_NONE}. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. - * @param id The content id, or {@code null} to set the manual stop reason for all downloads. - * @param manualStopReason An application defined stop reason. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. * @param foreground Whether the service is started in the foreground. */ - public static void sendManualStopReason( + public static void sendStopReason( Context context, Class clazz, @Nullable String id, - int manualStopReason, + int stopReason, boolean foreground) { - Intent intent = - buildSetManualStopReasonIntent(context, clazz, id, manualStopReason, foreground); + Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground); startService(context, intent, foreground); } @@ -481,9 +473,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (downloadRequest == null) { Log.e(TAG, "Ignored ADD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { - int manualStopReason = - intent.getIntExtra(KEY_MANUAL_STOP_REASON, Download.MANUAL_STOP_REASON_NONE); - downloadManager.addDownload(downloadRequest, manualStopReason); + int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + downloadManager.addDownload(downloadRequest, stopReason); } break; case ACTION_START: @@ -492,15 +483,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { case ACTION_STOP: downloadManager.stopDownloads(); break; - case ACTION_SET_MANUAL_STOP_REASON: - if (!intent.hasExtra(KEY_MANUAL_STOP_REASON)) { - Log.e( - TAG, "Ignored SET_MANUAL_STOP_REASON: Missing " + KEY_MANUAL_STOP_REASON + " extra"); + case ACTION_SET_STOP_REASON: + if (!intent.hasExtra(KEY_STOP_REASON)) { + Log.e(TAG, "Ignored SET_MANUAL_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { String contentId = intent.getStringExtra(KEY_CONTENT_ID); - int manualStopReason = - intent.getIntExtra(KEY_MANUAL_STOP_REASON, Download.MANUAL_STOP_REASON_NONE); - downloadManager.setManualStopReason(contentId, manualStopReason); + int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + downloadManager.setStopReason(contentId, stopReason); } break; case ACTION_REMOVE: 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 24f4421bc43..2306363cf55 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 @@ -36,24 +36,24 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void removeDownload(String id) throws IOException; /** - * Sets the manual stop reason of the downloads in a terminal state ({@link - * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, + * {@link Download#STATE_FAILED}). * - * @param manualStopReason The manual stop reason. + * @param stopReason The stop reason. * @throws throws IOException If an error occurs updating the state. */ - void setManualStopReason(int manualStopReason) throws IOException; + void setStopReason(int stopReason) throws IOException; /** - * Sets the manual stop reason of the download with the given {@code id} in a terminal state - * ({@link Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). + * Sets the stop reason of the download with the given {@code id} in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). * *

    If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, * then nothing happens. * * @param id ID of a {@link Download}. - * @param manualStopReason The manual stop reason. + * @param stopReason The stop reason. * @throws throws IOException If an error occurs updating the state. */ - void setManualStopReason(String id, int manualStopReason) throws IOException; + void setStopReason(String id, int stopReason) throws IOException; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index 5bd1f34ed40..a426a7488b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -79,7 +79,7 @@ public void addAndGetDownload_existingId_returnsUpdatedDownload() throws Databas .setDownloadedBytes(200) .setTotalBytes(400) .setFailureReason(Download.FAILURE_REASON_UNKNOWN) - .setManualStopReason(0x12345678) + .setStopReason(0x12345678) .setStartTimeMs(10) .setUpdateTimeMs(20) .setStreamKeys( @@ -204,23 +204,22 @@ public void downloadIndex_versionDowngradeWipesData() throws DatabaseIOException } @Test - public void setManualStopReason_setReasonToNone() throws Exception { + public void setStopReason_setReasonToNone() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = - new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setManualStopReason(0x12345678); + new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setStopReason(0x12345678); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); - downloadIndex.setManualStopReason(Download.MANUAL_STOP_REASON_NONE); + downloadIndex.setStopReason(Download.STOP_REASON_NONE); Download readDownload = downloadIndex.getDownload(id); - Download expectedDownload = - downloadBuilder.setManualStopReason(Download.MANUAL_STOP_REASON_NONE).build(); + Download expectedDownload = downloadBuilder.setStopReason(Download.STOP_REASON_NONE).build(); assertEqual(readDownload, expectedDownload); } @Test - public void setManualStopReason_setReason() throws Exception { + public void setStopReason_setReason() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = new DownloadBuilder(id) @@ -228,47 +227,46 @@ public void setManualStopReason_setReason() throws Exception { .setFailureReason(Download.FAILURE_REASON_UNKNOWN); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); - int manualStopReason = 0x12345678; + int stopReason = 0x12345678; - downloadIndex.setManualStopReason(manualStopReason); + downloadIndex.setStopReason(stopReason); Download readDownload = downloadIndex.getDownload(id); - Download expectedDownload = downloadBuilder.setManualStopReason(manualStopReason).build(); + Download expectedDownload = downloadBuilder.setStopReason(stopReason).build(); assertEqual(readDownload, expectedDownload); } @Test - public void setManualStopReason_notTerminalState_doesNotSetManualStopReason() throws Exception { + public void setStopReason_notTerminalState_doesNotSetStopReason() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = new DownloadBuilder(id).setState(Download.STATE_DOWNLOADING); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); int notMetRequirements = 0x12345678; - downloadIndex.setManualStopReason(notMetRequirements); + downloadIndex.setStopReason(notMetRequirements); Download readDownload = downloadIndex.getDownload(id); assertEqual(readDownload, download); } @Test - public void setSingleDownloadManualStopReason_setReasonToNone() throws Exception { + public void setSingleDownloadStopReason_setReasonToNone() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = - new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setManualStopReason(0x12345678); + new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setStopReason(0x12345678); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); - downloadIndex.setManualStopReason(id, Download.MANUAL_STOP_REASON_NONE); + downloadIndex.setStopReason(id, Download.STOP_REASON_NONE); Download readDownload = downloadIndex.getDownload(id); - Download expectedDownload = - downloadBuilder.setManualStopReason(Download.MANUAL_STOP_REASON_NONE).build(); + Download expectedDownload = downloadBuilder.setStopReason(Download.STOP_REASON_NONE).build(); assertEqual(readDownload, expectedDownload); } @Test - public void setSingleDownloadManualStopReason_setReason() throws Exception { + public void setSingleDownloadStopReason_setReason() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = new DownloadBuilder(id) @@ -276,25 +274,24 @@ public void setSingleDownloadManualStopReason_setReason() throws Exception { .setFailureReason(Download.FAILURE_REASON_UNKNOWN); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); - int manualStopReason = 0x12345678; + int stopReason = 0x12345678; - downloadIndex.setManualStopReason(id, manualStopReason); + downloadIndex.setStopReason(id, stopReason); Download readDownload = downloadIndex.getDownload(id); - Download expectedDownload = downloadBuilder.setManualStopReason(manualStopReason).build(); + Download expectedDownload = downloadBuilder.setStopReason(stopReason).build(); assertEqual(readDownload, expectedDownload); } @Test - public void setSingleDownloadManualStopReason_notTerminalState_doesNotSetManualStopReason() - throws Exception { + public void setSingleDownloadStopReason_notTerminalState_doesNotSetStopReason() throws Exception { String id = "id"; DownloadBuilder downloadBuilder = new DownloadBuilder(id).setState(Download.STATE_DOWNLOADING); Download download = downloadBuilder.build(); downloadIndex.putDownload(download); int notMetRequirements = 0x12345678; - downloadIndex.setManualStopReason(id, notMetRequirements); + downloadIndex.setStopReason(id, notMetRequirements); Download readDownload = downloadIndex.getDownload(id); assertEqual(readDownload, download); @@ -306,7 +303,7 @@ private static void assertEqual(Download download, Download that) { assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs); assertThat(download.failureReason).isEqualTo(that.failureReason); - assertThat(download.manualStopReason).isEqualTo(that.manualStopReason); + assertThat(download.stopReason).isEqualTo(that.stopReason); assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java index 2e14caa5bda..b5d84fa4bcd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java @@ -37,7 +37,7 @@ class DownloadBuilder { @Nullable private String cacheKey; private int state; private int failureReason; - private int manualStopReason; + private int stopReason; private long startTimeMs; private long updateTimeMs; private List streamKeys; @@ -127,8 +127,8 @@ public DownloadBuilder setFailureReason(int failureReason) { return this; } - public DownloadBuilder setManualStopReason(int manualStopReason) { - this.manualStopReason = manualStopReason; + public DownloadBuilder setStopReason(int stopReason) { + this.stopReason = stopReason; return this; } @@ -156,6 +156,6 @@ public Download build() { DownloadRequest request = new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata); return new Download( - request, state, failureReason, manualStopReason, startTimeMs, updateTimeMs, counters); + request, state, failureReason, stopReason, startTimeMs, updateTimeMs, counters); } } 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 140347bd910..2909bfd7790 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 @@ -57,7 +57,7 @@ public class DownloadManagerTest { private static final int MAX_RETRY_DELAY = 5000; /** Maximum number of times a downloader can be restarted before doing a released check. */ private static final int MAX_STARTS_BEFORE_RELEASED = 1; - /** A manual stop reason. */ + /** A stop reason. */ private static final int APP_STOP_REASON = 1; /** The minimum number of times a task must be retried before failing. */ private static final int MIN_RETRY_COUNT = 3; @@ -401,12 +401,11 @@ public void manuallyStopAndResumeSingleDownload() throws Throwable { task.assertDownloading(); - runOnMainThread(() -> downloadManager.setManualStopReason(task.taskId, APP_STOP_REASON)); + runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON)); task.assertStopped(); - runOnMainThread( - () -> downloadManager.setManualStopReason(task.taskId, Download.MANUAL_STOP_REASON_NONE)); + runOnMainThread(() -> downloadManager.setStopReason(task.taskId, Download.STOP_REASON_NONE)); runner.getDownloader(1).assertStarted().unblock(); @@ -420,7 +419,7 @@ public void manuallyStoppedDownloadCanBeCancelled() throws Throwable { task.assertDownloading(); - runOnMainThread(() -> downloadManager.setManualStopReason(task.taskId, APP_STOP_REASON)); + runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON)); task.assertStopped(); @@ -440,8 +439,7 @@ public void manuallyStoppedSingleDownload_doesNotAffectOthers() throws Throwable runner1.postDownloadRequest().getTask().assertDownloading(); runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving(); - runOnMainThread( - () -> downloadManager.setManualStopReason(runner1.getTask().taskId, APP_STOP_REASON)); + runOnMainThread(() -> downloadManager.setStopReason(runner1.getTask().taskId, APP_STOP_REASON)); runner1.getTask().assertStopped(); @@ -462,7 +460,7 @@ public void mergeRequest_removingDownload_becomesRestarting() { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.manualStopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); Download expectedDownload = downloadBuilder.setState(Download.STATE_RESTARTING).build(); assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); @@ -478,7 +476,7 @@ public void mergeRequest_failedDownload_becomesQueued() { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.manualStopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); Download expectedDownload = downloadBuilder @@ -494,26 +492,26 @@ public void mergeRequest_stoppedDownload_staysStopped() { DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) .setState(Download.STATE_STOPPED) - .setManualStopReason(/* manualStopReason= */ 1); + .setStopReason(/* stopReason= */ 1); Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.manualStopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); assertEqualIgnoringTimeFields(mergedDownload, download); } @Test - public void mergeRequest_manualStopReasonSetButNotStopped_becomesStopped() { + public void mergeRequest_stopReasonSetButNotStopped_becomesStopped() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) .setState(Download.STATE_COMPLETED) - .setManualStopReason(/* manualStopReason= */ 1); + .setStopReason(/* stopReason= */ 1); Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.manualStopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); Download expectedDownload = downloadBuilder.setState(Download.STATE_STOPPED).build(); assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); @@ -560,7 +558,7 @@ private static void assertEqualIgnoringTimeFields(Download download, Download th assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); assertThat(download.failureReason).isEqualTo(that.failureReason); - assertThat(download.manualStopReason).isEqualTo(that.manualStopReason); + assertThat(download.stopReason).isEqualTo(that.stopReason); assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); From 8c624081201b418c51747af753e41880d14f1edf Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 18:48:09 +0100 Subject: [PATCH 017/219] Rename start/stopDownloads to resume/pauseDownloads PiperOrigin-RevId: 244216620 --- .../exoplayer2/offline/DownloadManager.java | 34 ++--- .../exoplayer2/offline/DownloadService.java | 118 ++++++++++-------- .../offline/DownloadManagerTest.java | 6 +- .../dash/offline/DownloadManagerDashTest.java | 2 +- .../dash/offline/DownloadServiceDashTest.java | 2 +- 5 files changed, 91 insertions(+), 71 deletions(-) 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 497e3476afc..c34a5e233a0 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 @@ -55,8 +55,8 @@ * Manages downloads. * *

    Normally a download manager should be accessed via a {@link DownloadService}. When a download - * manager is used directly instead, downloads will be initially stopped and so must be started by - * calling {@link #startDownloads()}. + * manager is used directly instead, downloads will be initially paused and so must be resumed by + * calling {@link #resumeDownloads()}. * *

    A download manager instance must be accessed only from the thread that created it, unless that * thread does not have a {@link Looper}. In that case, it must be accessed only from the @@ -126,7 +126,7 @@ default void onRequirementsStateChanged( // Messages posted to the background handler. private static final int MSG_INITIALIZE = 0; - private static final int MSG_SET_DOWNLOADS_STARTED = 1; + private static final int MSG_SET_DOWNLOADS_RESUMED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; private static final int MSG_SET_STOP_REASON = 3; private static final int MSG_ADD_DOWNLOAD = 4; @@ -179,7 +179,7 @@ default void onRequirementsStateChanged( // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; - private boolean downloadsStarted; + private boolean downloadsResumed; private int simultaneousDownloads; /** @@ -346,19 +346,19 @@ public List getCurrentDownloads() { return Collections.unmodifiableList(new ArrayList<>(downloads)); } - /** Starts all downloads except those that have a non-zero {@link Download#stopReason}. */ - public void startDownloads() { + /** Resumes all downloads except those that have a non-zero {@link Download#stopReason}. */ + public void resumeDownloads() { pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_STARTED, /* downloadsStarted */ 1, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 1, /* unused */ 0) .sendToTarget(); } - /** Stops all downloads. */ - public void stopDownloads() { + /** Pauses all downloads. */ + public void pauseDownloads() { pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_STARTED, /* downloadsStarted */ 0, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 0, /* unused */ 0) .sendToTarget(); } @@ -541,9 +541,9 @@ private boolean handleInternalMessage(Message message) { int notMetRequirements = message.arg1; initializeInternal(notMetRequirements); break; - case MSG_SET_DOWNLOADS_STARTED: - boolean downloadsStarted = message.arg1 != 0; - setDownloadsStarted(downloadsStarted); + case MSG_SET_DOWNLOADS_RESUMED: + boolean downloadsResumed = message.arg1 != 0; + setDownloadsResumed(downloadsResumed); break; case MSG_SET_NOT_MET_REQUIREMENTS: notMetRequirements = message.arg1; @@ -604,11 +604,11 @@ private void initializeInternal(int notMetRequirements) { } } - private void setDownloadsStarted(boolean downloadsStarted) { - if (this.downloadsStarted == downloadsStarted) { + private void setDownloadsResumed(boolean downloadsResumed) { + if (this.downloadsResumed == downloadsResumed) { return; } - this.downloadsStarted = downloadsStarted; + this.downloadsResumed = downloadsResumed; for (int i = 0; i < downloadInternals.size(); i++) { downloadInternals.get(i).updateStopState(); } @@ -813,7 +813,7 @@ private void addDownloadForState(Download download) { } private boolean canStartDownloads() { - return downloadsStarted && notMetRequirements == 0; + return downloadsResumed && notMetRequirements == 0; } /* package */ static Download mergeRequest( 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 fa74afacb3f..9de6c748fb1 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 @@ -66,24 +66,24 @@ public abstract class DownloadService extends Service { public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; /** - * Starts all downloads except those that have a non-zero {@link Download#stopReason}. Extras: + * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * *

      *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ - public static final String ACTION_START = - "com.google.android.exoplayer.downloadService.action.START"; + public static final String ACTION_RESUME = + "com.google.android.exoplayer.downloadService.action.RESUME"; /** - * Stops all downloads. Extras: + * Pauses all downloads. Extras: * *
      *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ - public static final String ACTION_STOP = - "com.google.android.exoplayer.downloadService.action.STOP"; + public static final String ACTION_PAUSE = + "com.google.android.exoplayer.downloadService.action.PAUSE"; /** * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link @@ -278,51 +278,51 @@ public static Intent buildRemoveDownloadIntent( } /** - * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the - * stop reason, pass {@link Download#STOP_REASON_NONE}. + * Builds an {@link Intent} for resuming all downloads. * * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. - * @param id The content id, or {@code null} to set the stop reason for all downloads. - * @param stopReason An application defined stop reason. * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildSetStopReasonIntent( - Context context, - Class clazz, - @Nullable String id, - int stopReason, - boolean foreground) { - return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground) - .putExtra(KEY_CONTENT_ID, id) - .putExtra(KEY_STOP_REASON, stopReason); + public static Intent buildResumeDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_RESUME, foreground); } /** - * Builds an {@link Intent} for starting all downloads. + * Builds an {@link Intent} to pause 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 Created Intent. */ - public static Intent buildStartDownloadsIntent( + public static Intent buildPauseDownloadsIntent( Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_START, foreground); + return getIntent(context, clazz, ACTION_PAUSE, foreground); } /** - * Builds an {@link Intent} for stopping all downloads. + * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the + * stop reason, pass {@link Download#STOP_REASON_NONE}. * * @param context A {@link Context}. * @param clazz The concrete download service being targeted by the intent. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildStopDownloadsIntent( - Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_STOP, foreground); + public static Intent buildSetStopReasonIntent( + Context context, + Class clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground) + .putExtra(KEY_CONTENT_ID, id) + .putExtra(KEY_STOP_REASON, stopReason); } /** @@ -342,6 +342,26 @@ public static void sendNewDownload( startService(context, intent, foreground); } + /** + * Starts the service if not started already and adds a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param downloadRequest The request to be executed. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendNewDownload( + Context context, + Class clazz, + DownloadRequest downloadRequest, + int stopReason, + boolean foreground) { + Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, stopReason, foreground); + startService(context, intent, foreground); + } + /** * Starts the service if not started already and removes a download. * @@ -357,48 +377,48 @@ public static void sendRemoveDownload( } /** - * Starts the service if not started already and sets the stop reason for one or all downloads. To - * clear stop reason, pass {@link Download#STOP_REASON_NONE}. + * Starts the service if not started already and resumes all downloads. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. - * @param id The content id, or {@code null} to set the stop reason for all downloads. - * @param stopReason An application defined stop reason. * @param foreground Whether the service is started in the foreground. */ - public static void sendStopReason( - Context context, - Class clazz, - @Nullable String id, - int stopReason, - boolean foreground) { - Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground); + public static void sendResumeDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildResumeDownloadsIntent(context, clazz, foreground); startService(context, intent, foreground); } /** - * Starts the service if not started already and starts all downloads. + * Starts the service if not started already and pauses 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 sendStartDownloads( + public static void sendPauseDownloads( Context context, Class clazz, boolean foreground) { - Intent intent = buildStartDownloadsIntent(context, clazz, foreground); + Intent intent = buildPauseDownloadsIntent(context, clazz, foreground); startService(context, intent, foreground); } /** - * Starts the service if not started already and stops all downloads. + * Starts the service if not started already and sets the stop reason for one or all downloads. To + * clear stop reason, pass {@link Download#STOP_REASON_NONE}. * * @param context A {@link Context}. * @param clazz The concrete download service to be started. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. * @param foreground Whether the service is started in the foreground. */ - public static void sendStopDownloads( - Context context, Class clazz, boolean foreground) { - Intent intent = buildStopDownloadsIntent(context, clazz, foreground); + public static void sendStopReason( + Context context, + Class clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground); startService(context, intent, foreground); } @@ -438,7 +458,7 @@ public void onCreate() { DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); if (downloadManagerHelper == null) { DownloadManager downloadManager = getDownloadManager(); - downloadManager.startDownloads(); + downloadManager.resumeDownloads(); downloadManagerHelper = new DownloadManagerHelper( getApplicationContext(), downloadManager, getScheduler(), clazz); @@ -477,11 +497,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { downloadManager.addDownload(downloadRequest, stopReason); } break; - case ACTION_START: - downloadManager.startDownloads(); + case ACTION_RESUME: + downloadManager.resumeDownloads(); break; - case ACTION_STOP: - downloadManager.stopDownloads(); + case ACTION_PAUSE: + downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: if (!intent.hasExtra(KEY_STOP_REASON)) { 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 2909bfd7790..b1864165b31 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 @@ -368,7 +368,7 @@ public void stopAndResume() throws Throwable { runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving(); runner2.postDownloadRequest(); - runOnMainThread(() -> downloadManager.stopDownloads()); + runOnMainThread(() -> downloadManager.pauseDownloads()); runner1.getTask().assertStopped(); @@ -386,7 +386,7 @@ public void stopAndResume() throws Throwable { // New download requests can be added but they don't start. runner3.postDownloadRequest().getDownloader(0).assertDoesNotStart(); - runOnMainThread(() -> downloadManager.startDownloads()); + runOnMainThread(() -> downloadManager.resumeDownloads()); runner2.getDownloader(2).assertStarted().unblock(); runner3.getDownloader(0).assertStarted().unblock(); @@ -532,7 +532,7 @@ private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Excep maxActiveDownloadTasks, MIN_RETRY_COUNT, new Requirements(0)); - downloadManager.startDownloads(); + downloadManager.resumeDownloads(); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); }); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 0dce24bf1d4..02af54836cd 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -268,7 +268,7 @@ private void createDownloadManager() { downloadManagerListener = new TestDownloadManagerListener( downloadManager, dummyMainThread, /* timeout= */ 3000); - downloadManager.startDownloads(); + downloadManager.resumeDownloads(); }); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index a35b6d1ea46..b2b42c987e2 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -126,7 +126,7 @@ public void setUp() throws IOException { new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); - dashDownloadManager.startDownloads(); + dashDownloadManager.resumeDownloads(); dashDownloadService = new DownloadService(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) { From 38c5350c2cfbb86264081db7d367584f80f32044 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 19:19:38 +0100 Subject: [PATCH 018/219] Simplify DownloadManager constructors PiperOrigin-RevId: 244223870 --- .../exoplayer2/demo/DemoApplication.java | 8 +- .../exoplayer2/offline/DownloadManager.java | 105 +++++++----------- .../offline/DownloadManagerTest.java | 12 +- .../dash/offline/DownloadManagerDashTest.java | 6 +- .../dash/offline/DownloadServiceDashTest.java | 6 +- 5 files changed, 46 insertions(+), 91 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 446184e56bd..2c9cd43d1e3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -49,7 +49,6 @@ public class DemoApplication extends Application { private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; - private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; protected String userAgent; @@ -122,12 +121,7 @@ private synchronized void initDownloadManager() { new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( - this, - downloadIndex, - new DefaultDownloaderFactory(downloaderConstructorHelper), - MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT, - DownloadManager.DEFAULT_REQUIREMENTS); + this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); downloadTracker = new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); } 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 c34a5e233a0..f914c861f90 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 @@ -34,7 +34,6 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; @@ -111,8 +110,8 @@ default void onRequirementsStateChanged( @Requirements.RequirementFlags int notMetRequirements) {} } - /** The default maximum number of simultaneous downloads. */ - public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1; + /** The default maximum number of parallel downloads. */ + public static final int DEFAULT_MAX_PARALLEL_DOWNLOADS = 3; /** The default minimum number of times a download must be retried before failing. */ public static final int DEFAULT_MIN_RETRY_COUNT = 5; /** The default requirement is that the device has network connectivity. */ @@ -151,8 +150,6 @@ default void onRequirementsStateChanged( private static final String TAG = "DownloadManager"; private static final boolean DEBUG = false; - private final int maxSimultaneousDownloads; - private final int minRetryCount; private final Context context; private final WritableDownloadIndex downloadIndex; private final DownloaderFactory downloaderFactory; @@ -180,51 +177,11 @@ default void onRequirementsStateChanged( // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; private boolean downloadsResumed; - private int simultaneousDownloads; + private int parallelDownloads; - /** - * Constructs a {@link DownloadManager}. - * - * @param context Any context. - * @param databaseProvider Provides the database that holds the downloads. - * @param downloaderFactory A factory for creating {@link Downloader}s. - */ - public DownloadManager( - Context context, DatabaseProvider databaseProvider, DownloaderFactory downloaderFactory) { - this( - context, - databaseProvider, - downloaderFactory, - DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, - DEFAULT_MIN_RETRY_COUNT, - DEFAULT_REQUIREMENTS); - } - - /** - * Constructs a {@link DownloadManager}. - * - * @param context Any context. - * @param databaseProvider Provides the database that holds the downloads. - * @param downloaderFactory A factory for creating {@link Downloader}s. - * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. - * @param minRetryCount The minimum number of times a download must be retried before failing. - * @param requirements The requirements needed to be met to start downloads. - */ - public DownloadManager( - Context context, - DatabaseProvider databaseProvider, - DownloaderFactory downloaderFactory, - int maxSimultaneousDownloads, - int minRetryCount, - Requirements requirements) { - this( - context, - new DefaultDownloadIndex(databaseProvider), - downloaderFactory, - maxSimultaneousDownloads, - minRetryCount, - requirements); - } + // TODO: Fix these to properly support changes at runtime. + private volatile int maxParallelDownloads; + private volatile int minRetryCount; /** * Constructs a {@link DownloadManager}. @@ -232,22 +189,14 @@ public DownloadManager( * @param context Any context. * @param downloadIndex The download index used to hold the download information. * @param downloaderFactory A factory for creating {@link Downloader}s. - * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. - * @param minRetryCount The minimum number of times a download must be retried before failing. - * @param requirements The requirements needed to be met to start downloads. */ public DownloadManager( - Context context, - WritableDownloadIndex downloadIndex, - DownloaderFactory downloaderFactory, - int maxSimultaneousDownloads, - int minRetryCount, - Requirements requirements) { + Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { this.context = context.getApplicationContext(); this.downloadIndex = downloadIndex; this.downloaderFactory = downloaderFactory; - this.maxSimultaneousDownloads = maxSimultaneousDownloads; - this.minRetryCount = minRetryCount; + maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; + minRetryCount = DEFAULT_MIN_RETRY_COUNT; downloadInternals = new ArrayList<>(); downloads = new ArrayList<>(); @@ -262,7 +211,8 @@ public DownloadManager( internalThread.start(); internalHandler = new Handler(internalThread.getLooper(), this::handleInternalMessage); - requirementsWatcher = new RequirementsWatcher(context, requirementsListener, requirements); + requirementsWatcher = + new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); int notMetRequirements = requirementsWatcher.start(); pendingMessages = 1; @@ -332,6 +282,27 @@ public void setRequirements(Requirements requirements) { onRequirementsStateChanged(requirementsWatcher, notMetRequirements); } + /** + * Sets the maximum number of parallel downloads. + * + * @param maxParallelDownloads The maximum number of parallel downloads. + */ + // TODO: Fix to properly support changes at runtime. + public void setMaxParallelDownloads(int maxParallelDownloads) { + this.maxParallelDownloads = maxParallelDownloads; + } + + /** + * Sets the minimum number of times that a download will be retried. A download will fail if the + * specified number of retries is exceeded without any progress being made. + * + * @param minRetryCount The minimum number of times that a download will be retried. + */ + // TODO: Fix to properly support changes at runtime. + public void setMinRetryCount(int minRetryCount) { + this.minRetryCount = minRetryCount; + } + /** Returns the used {@link DownloadIndex}. */ public DownloadIndex getDownloadIndex() { return downloadIndex; @@ -696,15 +667,15 @@ private void onDownloadThreadStoppedInternal(DownloadThread downloadThread) { downloadThreads.remove(downloadId); boolean tryToStartDownloads = false; if (!downloadThread.isRemove) { - // If maxSimultaneousDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = simultaneousDownloads == maxSimultaneousDownloads; - simultaneousDownloads--; + // If maxParallelDownloads was hit, there might be a download waiting for a slot. + tryToStartDownloads = parallelDownloads == maxParallelDownloads; + parallelDownloads--; } getDownload(downloadId) .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); if (tryToStartDownloads) { for (int i = 0; - simultaneousDownloads < maxSimultaneousDownloads && i < downloadInternals.size(); + parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); i++) { downloadInternals.get(i).start(); } @@ -760,10 +731,10 @@ private int startDownloadThread(DownloadInternal downloadInternal) { } boolean isRemove = downloadInternal.isInRemoveState(); if (!isRemove) { - if (simultaneousDownloads == maxSimultaneousDownloads) { + if (parallelDownloads == maxParallelDownloads) { return START_THREAD_TOO_MANY_DOWNLOADS; } - simultaneousDownloads++; + parallelDownloads++; } Downloader downloader = downloaderFactory.createDownloader(request); DownloadThread downloadThread = 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 b1864165b31..17328248c64 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 @@ -517,7 +517,7 @@ public void mergeRequest_stopReasonSetButNotStopped_becomesStopped() { assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); } - private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { + private void setUpDownloadManager(final int maxParallelDownloads) throws Exception { if (downloadManager != null) { releaseDownloadManager(); } @@ -526,12 +526,10 @@ private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Excep () -> { downloadManager = new DownloadManager( - ApplicationProvider.getApplicationContext(), - downloadIndex, - downloaderFactory, - maxActiveDownloadTasks, - MIN_RETRY_COUNT, - new Requirements(0)); + ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory); + downloadManager.setMaxParallelDownloads(maxParallelDownloads); + downloadManager.setMinRetryCount(MIN_RETRY_COUNT); + downloadManager.setRequirements(new Requirements(0)); downloadManager.resumeDownloads(); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 02af54836cd..9fc9834e1d9 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -260,10 +259,7 @@ private void createDownloadManager() { ApplicationProvider.getApplicationContext(), downloadIndex, new DefaultDownloaderFactory( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), - /* maxSimultaneousDownloads= */ 1, - /* minRetryCount= */ 3, - new Requirements(0)); + new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); downloadManagerListener = new TestDownloadManagerListener( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index b2b42c987e2..57e7b8de5fd 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -120,10 +119,7 @@ public void setUp() throws IOException { ApplicationProvider.getApplicationContext(), downloadIndex, new DefaultDownloaderFactory( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), - /* maxSimultaneousDownloads= */ 1, - /* minRetryCount= */ 3, - new Requirements(0)); + new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); downloadManagerListener = new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); dashDownloadManager.resumeDownloads(); From 7d67047e9472efad1291d3a2522edd913a784628 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 19:34:00 +0100 Subject: [PATCH 019/219] Support multiple DefaultDownloadIndex instances PiperOrigin-RevId: 244226680 --- .../offline/DefaultDownloadIndex.java | 69 +++++++++++-------- .../offline/DefaultDownloadIndexTest.java | 14 ++-- 2 files changed, 47 insertions(+), 36 deletions(-) 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 a2caff3ff11..30297f19cea 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 @@ -23,6 +23,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import android.text.TextUtils; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; @@ -32,18 +33,11 @@ import java.util.ArrayList; import java.util.List; -/** - * A {@link DownloadIndex} which uses SQLite to persist {@link Download}s. - * - *

    Database access may take a long time, do not call methods of this class from - * the application main thread. - */ +/** A {@link DownloadIndex} that uses SQLite to persist {@link Download Downloads}. */ public final class DefaultDownloadIndex implements WritableDownloadIndex { - private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads"; + private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "Downloads"; - // TODO: Support multiple instances. Probably using the underlying cache UID. - @VisibleForTesting /* package */ static final String INSTANCE_UID = "singleton"; @VisibleForTesting /* package */ static final int TABLE_VERSION = 1; private static final String COLUMN_ID = "id"; @@ -108,11 +102,8 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { COLUMN_UPDATE_TIME_MS }; - private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; - private static final String SQL_CREATE_TABLE = - "CREATE TABLE " - + TABLE_NAME - + " (" + private static final String TABLE_SCHEMA = + "(" + COLUMN_ID + " TEXT PRIMARY KEY NOT NULL," + COLUMN_TYPE @@ -148,19 +139,42 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final String TRUE = "1"; + private final String name; + private final String tableName; private final DatabaseProvider databaseProvider; private boolean initialized; /** - * Creates a DefaultDownloadIndex which stores the {@link Download}s on a SQLite database provided - * by {@code databaseProvider}. + * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided + * by a {@link DatabaseProvider}. + * + *

    Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code + * name=""}. * - * @param databaseProvider A DatabaseProvider which provides the database which will be used to - * store DownloadStatus table. + *

    Applications that only have one download index may use this constructor. Applications that + * have multiple download indices should call {@link #DefaultDownloadIndex(DatabaseProvider, + * String)} to specify a unique name for each index. + * + * @param databaseProvider Provides the SQLite database in which downloads are persisted. */ public DefaultDownloadIndex(DatabaseProvider databaseProvider) { + this(databaseProvider, ""); + } + + /** + * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided + * by a {@link DatabaseProvider}. + * + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param name The name of the index. This name is incorporated into the names of the SQLite + * tables in which downloads are persisted. + */ + public DefaultDownloadIndex(DatabaseProvider databaseProvider, String name) { + // TODO: Remove this backward compatibility hack for launch. + this.name = TextUtils.isEmpty(name) ? "singleton" : name; this.databaseProvider = databaseProvider; + tableName = TABLE_PREFIX + name; } @Override @@ -207,7 +221,7 @@ public void putDownload(Download download) throws DatabaseIOException { values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values); + writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); } catch (SQLiteException e) { throw new DatabaseIOException(e); } @@ -217,7 +231,7 @@ public void putDownload(Download download) throws DatabaseIOException { public void removeDownload(String id) throws DatabaseIOException { ensureInitialized(); try { - databaseProvider.getWritableDatabase().delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {id}); + databaseProvider.getWritableDatabase().delete(tableName, WHERE_ID_EQUALS, new String[] {id}); } catch (SQLiteException e) { throw new DatabaseIOException(e); } @@ -230,7 +244,7 @@ public void setStopReason(int stopReason) throws DatabaseIOException { ContentValues values = new ContentValues(); values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.update(TABLE_NAME, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); + writableDatabase.update(tableName, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); } catch (SQLException e) { throw new DatabaseIOException(e); } @@ -244,7 +258,7 @@ public void setStopReason(String id, int stopReason) throws DatabaseIOException values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update( - TABLE_NAME, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); + tableName, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); } catch (SQLException e) { throw new DatabaseIOException(e); } @@ -256,16 +270,15 @@ private void ensureInitialized() throws DatabaseIOException { } try { SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); - int version = - VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID); + int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name); if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); try { VersionTable.setVersion( - writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, TABLE_VERSION); - writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); - writableDatabase.execSQL(SQL_CREATE_TABLE); + writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION); + writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName); + writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); @@ -287,7 +300,7 @@ private Cursor getCursor(String selection, @Nullable String[] selectionArgs) return databaseProvider .getReadableDatabase() .query( - TABLE_NAME, + tableName, COLUMNS, selection, selectionArgs, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index a426a7488b9..73c73b6647d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.offline; -import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.INSTANCE_UID; import static com.google.common.truth.Truth.assertThat; import android.database.sqlite.SQLiteDatabase; @@ -33,6 +32,8 @@ @RunWith(AndroidJUnit4.class) public class DefaultDownloadIndexTest { + private static final String EMPTY_NAME = "singleton"; + private ExoDatabaseProvider databaseProvider; private DefaultDownloadIndex downloadIndex; @@ -170,14 +171,12 @@ public void getDownloads_withStates_returnsAllDownloadStatusWithTheSameStates() @Test public void putDownload_setsVersion() throws DatabaseIOException { SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); - assertThat( - VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID)) + assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME)) .isEqualTo(VersionTable.VERSION_UNSET); downloadIndex.putDownload(new DownloadBuilder("id1").build()); - assertThat( - VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID)) + assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME)) .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } @@ -191,15 +190,14 @@ public void downloadIndex_versionDowngradeWipesData() throws DatabaseIOException SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); VersionTable.setVersion( - writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, Integer.MAX_VALUE); + writableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME, Integer.MAX_VALUE); downloadIndex = new DefaultDownloadIndex(databaseProvider); cursor = downloadIndex.getDownloads(); assertThat(cursor.getCount()).isEqualTo(0); cursor.close(); - assertThat( - VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID)) + assertThat(VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME)) .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } From 54a5d6912bfd516091279fcd23d8984709819485 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Apr 2019 21:02:48 +0100 Subject: [PATCH 020/219] Improve progress reporting logic - Listener based reporting of progress allows the content length to be persisted into the download index (and notified via a download state change) as soon as it's available. - Moved contentLength back into Download proper. It should only ever change once, so I'm not sure it belongs in the mutable part of Download. - Made a DownloadProgress class, for naming sanity. PiperOrigin-RevId: 244242487 --- .../offline/ActionFileUpgradeUtil.java | 8 +- .../offline/DefaultDownloadIndex.java | 27 ++- .../android/exoplayer2/offline/Download.java | 67 +++--- .../exoplayer2/offline/DownloadManager.java | 107 ++++++--- .../exoplayer2/offline/DownloadProgress.java | 28 +++ .../exoplayer2/offline/Downloader.java | 48 ++-- .../offline/ProgressiveDownloader.java | 40 ++-- .../exoplayer2/offline/SegmentDownloader.java | 208 +++++++++--------- .../exoplayer2/upstream/cache/CacheUtil.java | 137 ++++++------ .../offline/DefaultDownloadIndexTest.java | 14 +- .../exoplayer2/offline/DownloadBuilder.java | 70 +++--- .../offline/DownloadManagerTest.java | 44 ++-- .../upstream/cache/CacheDataSourceTest.java | 8 +- .../upstream/cache/CacheUtilTest.java | 138 +++++++----- .../source/dash/offline/DashDownloader.java | 4 +- .../dash/offline/DashDownloaderTest.java | 46 ++-- .../source/hls/offline/HlsDownloader.java | 4 +- .../source/hls/offline/HlsDownloaderTest.java | 36 ++- .../smoothstreaming/offline/SsDownloader.java | 4 +- .../ui/DownloadNotificationHelper.java | 4 +- .../playbacktests/gts/DashDownloadTest.java | 2 +- 21 files changed, 587 insertions(+), 457 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 51996ed2843..b601874f8de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -18,6 +18,7 @@ import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -97,10 +98,11 @@ public static void upgradeAndDelete( new Download( request, STATE_QUEUED, - Download.FAILURE_REASON_NONE, - Download.STOP_REASON_NONE, /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs); + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + Download.STOP_REASON_NONE, + Download.FAILURE_REASON_NONE); } downloadIndex.putDownload(download); } 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 30297f19cea..6838c246283 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 @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -210,15 +209,15 @@ public void putDownload(Download download) throws DatabaseIOException { values.put(COLUMN_CUSTOM_CACHE_KEY, download.request.customCacheKey); values.put(COLUMN_DATA, download.request.data); values.put(COLUMN_STATE, download.state); - values.put(COLUMN_DOWNLOAD_PERCENTAGE, download.getDownloadPercentage()); - values.put(COLUMN_DOWNLOADED_BYTES, download.getDownloadedBytes()); - values.put(COLUMN_TOTAL_BYTES, download.getTotalBytes()); + values.put(COLUMN_START_TIME_MS, download.startTimeMs); + values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); + values.put(COLUMN_TOTAL_BYTES, download.contentLength); + values.put(COLUMN_STOP_REASON, download.stopReason); values.put(COLUMN_FAILURE_REASON, download.failureReason); + values.put(COLUMN_DOWNLOAD_PERCENTAGE, download.getPercentDownloaded()); + values.put(COLUMN_DOWNLOADED_BYTES, download.getBytesDownloaded()); values.put(COLUMN_STOP_FLAGS, 0); values.put(COLUMN_NOT_MET_REQUIREMENTS, 0); - values.put(COLUMN_STOP_REASON, download.stopReason); - values.put(COLUMN_START_TIME_MS, download.startTimeMs); - values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); @@ -337,18 +336,18 @@ private static Download getDownloadForCurrentRow(Cursor cursor) { decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY), cursor.getBlob(COLUMN_INDEX_DATA)); - CachingCounters cachingCounters = new CachingCounters(); - cachingCounters.alreadyCachedBytes = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES); - cachingCounters.contentLength = cursor.getLong(COLUMN_INDEX_TOTAL_BYTES); - cachingCounters.percentage = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE); + DownloadProgress downloadProgress = new DownloadProgress(); + downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES); + downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE); return new Download( request, cursor.getInt(COLUMN_INDEX_STATE), - cursor.getInt(COLUMN_INDEX_FAILURE_REASON), - cursor.getInt(COLUMN_INDEX_STOP_REASON), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), - cachingCounters); + cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), + cursor.getInt(COLUMN_INDEX_STOP_REASON), + cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + downloadProgress); } private static String encodeStreamKeys(List streamKeys) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 343b9d6a496..9f6b473208b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -17,7 +17,6 @@ import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -96,60 +95,65 @@ public static String getStateString(@State int state) { /** The download request. */ public final DownloadRequest request; - /** The state of the download. */ @State public final int state; /** The first time when download entry is created. */ public final long startTimeMs; /** The last update time. */ public final long updateTimeMs; + /** The total size of the content in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + public final long contentLength; + /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ + public final int stopReason; /** * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link * #FAILURE_REASON_NONE}. */ @FailureReason public final int failureReason; - /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ - public final int stopReason; - /* package */ CachingCounters counters; + /* package */ final DownloadProgress progress; - /* package */ Download( + public Download( DownloadRequest request, @State int state, - @FailureReason int failureReason, - int stopReason, long startTimeMs, - long updateTimeMs) { + long updateTimeMs, + long contentLength, + int stopReason, + @FailureReason int failureReason) { this( request, state, - failureReason, - stopReason, startTimeMs, updateTimeMs, - new CachingCounters()); + contentLength, + stopReason, + failureReason, + new DownloadProgress()); } - /* package */ Download( + public Download( DownloadRequest request, @State int state, - @FailureReason int failureReason, - int stopReason, long startTimeMs, long updateTimeMs, - CachingCounters counters) { - Assertions.checkNotNull(counters); + long contentLength, + int stopReason, + @FailureReason int failureReason, + DownloadProgress progress) { + Assertions.checkNotNull(progress); Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); if (stopReason != 0) { Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; - this.failureReason = failureReason; - this.stopReason = stopReason; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; - this.counters = counters; + this.contentLength = contentLength; + this.stopReason = stopReason; + this.failureReason = failureReason; + this.progress = progress; } /** Returns whether the download is completed or failed. These are terminal states. */ @@ -158,30 +162,15 @@ public boolean isTerminalState() { } /** Returns the total number of downloaded bytes. */ - public long getDownloadedBytes() { - return counters.totalCachedBytes(); - } - - /** Returns the total size of the media, or {@link C#LENGTH_UNSET} if unknown. */ - public long getTotalBytes() { - return counters.contentLength; + public long getBytesDownloaded() { + return progress.bytesDownloaded; } /** * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is * available. */ - public float getDownloadPercentage() { - return counters.percentage; - } - - /** - * Sets counters which are updated by a {@link Downloader}. - * - * @param counters An instance of {@link CachingCounters}. - */ - protected void setCounters(CachingCounters counters) { - Assertions.checkNotNull(counters); - this.counters = counters; + public float getPercentDownloaded() { + return progress.percentDownloaded; } } 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 f914c861f90..d4df5cd18b5 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 @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -131,7 +130,8 @@ default void onRequirementsStateChanged( private static final int MSG_ADD_DOWNLOAD = 4; private static final int MSG_REMOVE_DOWNLOAD = 5; private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; - private static final int MSG_RELEASE = 7; + private static final int MSG_CONTENT_LENGTH_CHANGED = 7; + private static final int MSG_RELEASE = 8; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -539,6 +539,11 @@ private boolean handleInternalMessage(Message message) { onDownloadThreadStoppedInternal(downloadThread); processedExternalMessage = false; // This message is posted internally. break; + case MSG_CONTENT_LENGTH_CHANGED: + downloadThread = (DownloadThread) message.obj; + onDownloadThreadContentLengthChangedInternal(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; case MSG_RELEASE: releaseInternal(); return true; // Don't post back to mainHandler on release. @@ -634,10 +639,11 @@ private void addDownloadInternal(DownloadRequest request, int stopReason) { new Download( request, stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - Download.FAILURE_REASON_NONE, - stopReason, /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs); + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + Download.FAILURE_REASON_NONE); logd("Download state is created for " + request.id); } else { download = mergeRequest(download, request, stopReason); @@ -682,6 +688,11 @@ private void onDownloadThreadStoppedInternal(DownloadThread downloadThread) { } } + private void onDownloadThreadContentLengthChangedInternal(DownloadThread downloadThread) { + String downloadId = downloadThread.request.id; + getDownload(downloadId).setContentLength(downloadThread.contentLength); + } + private void releaseInternal() { for (DownloadThread downloadThread : downloadThreads.values()) { downloadThread.cancel(/* released= */ true); @@ -737,10 +748,11 @@ private int startDownloadThread(DownloadInternal downloadInternal) { parallelDownloads++; } Downloader downloader = downloaderFactory.createDownloader(request); + DownloadProgress downloadProgress = downloadInternal.download.progress; DownloadThread downloadThread = - new DownloadThread(request, downloader, isRemove, minRetryCount, internalHandler); + new DownloadThread( + request, downloader, downloadProgress, isRemove, minRetryCount, internalHandler); downloadThreads.put(downloadId, downloadThread); - downloadInternal.setCounters(downloadThread.downloader.getCounters()); downloadThread.start(); logd("Download is started", downloadInternal); return START_THREAD_SUCCEEDED; @@ -802,22 +814,23 @@ private boolean canStartDownloads() { return new Download( download.request.copyWithMergedRequest(request), state, - FAILURE_REASON_NONE, - stopReason, startTimeMs, /* updateTimeMs= */ nowMs, - download.counters); + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE); } private static Download copyWithState(Download download, @Download.State int state) { return new Download( download.request, state, - FAILURE_REASON_NONE, - download.stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - download.counters); + download.contentLength, + download.stopReason, + FAILURE_REASON_NONE, + download.progress); } private static void logd(String message) { @@ -850,13 +863,17 @@ private static final class DownloadInternal { // TODO: Get rid of these and use download directly. @Download.State private int state; + private long contentLength; private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; private DownloadInternal(DownloadManager downloadManager, Download download) { this.downloadManager = downloadManager; this.download = download; + state = download.state; + contentLength = download.contentLength; stopReason = download.stopReason; + failureReason = download.failureReason; } private void initialize() { @@ -877,11 +894,12 @@ public Download getUpdatedDownload() { new Download( download.request, state, - state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - download.counters); + contentLength, + stopReason, + state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, + download.progress); return download; } @@ -911,8 +929,12 @@ public boolean isInRemoveState() { return state == STATE_REMOVING || state == STATE_RESTARTING; } - public void setCounters(CachingCounters counters) { - download.setCounters(counters); + public void setContentLength(long contentLength) { + if (this.contentLength == contentLength) { + return; + } + this.contentLength = contentLength; + downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); } private void updateStopState() { @@ -992,28 +1014,34 @@ private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable err } } - private static class DownloadThread extends Thread { + private static class DownloadThread extends Thread implements Downloader.ProgressListener { private final DownloadRequest request; private final Downloader downloader; + private final DownloadProgress downloadProgress; private final boolean isRemove; private final int minRetryCount; - private volatile Handler onStoppedHandler; + private volatile Handler updateHandler; private volatile boolean isCanceled; private Throwable finalError; + private long contentLength; + private DownloadThread( DownloadRequest request, Downloader downloader, + DownloadProgress downloadProgress, boolean isRemove, int minRetryCount, - Handler onStoppedHandler) { + Handler updateHandler) { this.request = request; - this.isRemove = isRemove; this.downloader = downloader; + this.downloadProgress = downloadProgress; + this.isRemove = isRemove; this.minRetryCount = minRetryCount; - this.onStoppedHandler = onStoppedHandler; + this.updateHandler = updateHandler; + contentLength = C.LENGTH_UNSET; } public void cancel(boolean released) { @@ -1022,7 +1050,7 @@ public void cancel(boolean released) { // cancellation to complete depends on the implementation of the downloader being used. We // null the handler reference here so that it doesn't prevent garbage collection of the // download manager whilst cancellation is ongoing. - onStoppedHandler = null; + updateHandler = null; } isCanceled = true; downloader.cancel(); @@ -1042,14 +1070,14 @@ public void run() { long errorPosition = C.LENGTH_UNSET; while (!isCanceled) { try { - downloader.download(); + downloader.download(/* progressListener= */ this); break; } catch (IOException e) { if (!isCanceled) { - long downloadedBytes = downloader.getDownloadedBytes(); - if (downloadedBytes != errorPosition) { - logd("Reset error count. downloadedBytes = " + downloadedBytes, request); - errorPosition = downloadedBytes; + long bytesDownloaded = downloadProgress.bytesDownloaded; + if (bytesDownloaded != errorPosition) { + logd("Reset error count. bytesDownloaded = " + bytesDownloaded, request); + errorPosition = bytesDownloaded; errorCount = 0; } if (++errorCount > minRetryCount) { @@ -1064,13 +1092,26 @@ public void run() { } catch (Throwable e) { finalError = e; } - Handler onStoppedHandler = this.onStoppedHandler; - if (onStoppedHandler != null) { - onStoppedHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + Handler updateHandler = this.updateHandler; + if (updateHandler != null) { + updateHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + } + } + + @Override + public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) { + downloadProgress.bytesDownloaded = bytesDownloaded; + downloadProgress.percentDownloaded = percentDownloaded; + if (contentLength != this.contentLength) { + this.contentLength = contentLength; + Handler updateHandler = this.updateHandler; + if (updateHandler != null) { + updateHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + } } } - private int getRetryDelayMillis(int errorCount) { + private static int getRetryDelayMillis(int errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java new file mode 100644 index 00000000000..9d946daa286 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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.offline; + +import com.google.android.exoplayer2.C; + +/** Mutable {@link Download} progress. */ +public class DownloadProgress { + + /** The number of bytes that have been downloaded. */ + public long bytesDownloaded; + + /** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */ + public float percentDownloaded; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index 39f562ac19f..fa10d5842b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -15,44 +15,44 @@ */ package com.google.android.exoplayer2.offline; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import java.io.IOException; -/** - * An interface for stream downloaders. - */ +/** Downloads and removes a piece of content. */ public interface Downloader { + /** Receives progress updates during download operations. */ + interface ProgressListener { + + /** + * Called when progress is made during a download operation. + * + * @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if + * unknown. + * @param bytesDownloaded The number of bytes that have been downloaded. + * @param percentDownloaded The percentage of the content that has been downloaded, or {@link + * C#PERCENTAGE_UNSET}. + */ + void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded); + } + /** - * Downloads the media. + * Downloads the content. * - * @throws DownloadException Thrown if the media cannot be downloaded. + * @param progressListener A listener to receive progress updates, or {@code null}. + * @throws DownloadException Thrown if the content cannot be downloaded. * @throws InterruptedException If the thread has been interrupted. * @throws IOException Thrown when there is an io error while downloading. */ - void download() throws InterruptedException, IOException; + void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException; - /** Interrupts any current download operation and prevents future operations from running. */ + /** Cancels the download operation and prevents future download operations from running. */ void cancel(); - /** Returns the total number of downloaded bytes. */ - long getDownloadedBytes(); - - /** Returns the total size of the media, or {@link C#LENGTH_UNSET} if unknown. */ - long getTotalBytes(); - - /** - * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is - * available. - */ - float getDownloadPercentage(); - - /** Returns a {@link CachingCounters} which holds download counters. */ - CachingCounters getCounters(); - /** - * Removes the media. + * Removes the content. * * @throws InterruptedException Thrown if the thread was interrupted. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index 9794b19b622..17f4047bc04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,7 +39,6 @@ public final class ProgressiveDownloader implements Downloader { private final CacheDataSource dataSource; private final CacheKeyFactory cacheKeyFactory; private final PriorityTaskManager priorityTaskManager; - private final CacheUtil.CachingCounters cachingCounters; private final AtomicBoolean isCanceled; /** @@ -62,12 +60,12 @@ public ProgressiveDownloader( this.dataSource = constructorHelper.createCacheDataSource(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); - cachingCounters = new CachingCounters(); isCanceled = new AtomicBoolean(); } @Override - public void download() throws InterruptedException, IOException { + public void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); try { CacheUtil.cache( @@ -78,7 +76,7 @@ public void download() throws InterruptedException, IOException { new byte[BUFFER_SIZE_BYTES], priorityTaskManager, C.PRIORITY_DOWNLOAD, - cachingCounters, + progressListener == null ? null : new ProgressForwarder(progressListener), isCanceled, /* enableEOFException= */ true); } finally { @@ -92,27 +90,25 @@ public void cancel() { } @Override - public long getDownloadedBytes() { - return cachingCounters.totalCachedBytes(); + public void remove() { + CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } - @Override - public long getTotalBytes() { - return cachingCounters.contentLength; - } + private static final class ProgressForwarder implements CacheUtil.ProgressListener { - @Override - public float getDownloadPercentage() { - return cachingCounters.percentage; - } + private final ProgressListener progessListener; - @Override - public CachingCounters getCounters() { - return cachingCounters; - } + public ProgressForwarder(ProgressListener progressListener) { + this.progessListener = progressListener; + } - @Override - public void remove() { - CacheUtil.remove(dataSpec, cache, cacheKeyFactory); + @Override + public void onProgress(long contentLength, long bytesCached, long newBytesCached) { + float percentDownloaded = + contentLength == C.LENGTH_UNSET || contentLength == 0 + ? C.PERCENTAGE_UNSET + : ((bytesCached * 100f) / contentLength); + progessListener.onProgress(contentLength, bytesCached, percentDownloaded); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 4dbae47775e..1643812ece2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -17,6 +17,8 @@ import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -24,7 +26,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -42,6 +43,7 @@ public abstract class SegmentDownloader> impleme /** Smallest unit of content to be downloaded. */ protected static class Segment implements Comparable { + /** The start time of the segment in microseconds. */ public final long startTimeUs; @@ -70,10 +72,6 @@ public int compareTo(@NonNull Segment other) { private final PriorityTaskManager priorityTaskManager; private final ArrayList streamKeys; private final AtomicBoolean isCanceled; - private final CacheUtil.CachingCounters counters; - - private volatile int totalSegments; - private volatile int downloadedSegments; /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. @@ -90,9 +88,7 @@ public SegmentDownloader( this.offlineDataSource = constructorHelper.createOfflineCacheDataSource(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); - totalSegments = C.LENGTH_UNSET; isCanceled = new AtomicBoolean(); - counters = new CachingCounters(); } /** @@ -102,35 +98,71 @@ public SegmentDownloader( * @throws IOException Thrown when there is an error downloading. * @throws InterruptedException If the thread has been interrupted. */ - // downloadedSegments and downloadedBytes are only written from this method, and this method - // should not be called from more than one thread. Hence non-atomic updates are valid. - @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public final void download() throws IOException, InterruptedException { + public final void download(@Nullable ProgressListener progressListener) + throws IOException, InterruptedException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); - try { - List segments = initDownload(); + // Get the manifest and all of the segments. + M manifest = getManifest(dataSource, manifestDataSpec); + if (!streamKeys.isEmpty()) { + manifest = manifest.copy(streamKeys); + } + List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); + + // Scan the segments, removing any that are fully downloaded. + int totalSegments = segments.size(); + int segmentsDownloaded = 0; + long contentLength = 0; + long bytesDownloaded = 0; + for (int i = segments.size() - 1; i >= 0; i--) { + Segment segment = segments.get(i); + Pair segmentLengthAndBytesDownloaded = + CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory); + long segmentLength = segmentLengthAndBytesDownloaded.first; + long segmentBytesDownloaded = segmentLengthAndBytesDownloaded.second; + bytesDownloaded += segmentBytesDownloaded; + if (segmentLength != C.LENGTH_UNSET) { + if (segmentLength == segmentBytesDownloaded) { + // The segment is fully downloaded. + segmentsDownloaded++; + segments.remove(i); + } + if (contentLength != C.LENGTH_UNSET) { + contentLength += segmentLength; + } + } else { + contentLength = C.LENGTH_UNSET; + } + } Collections.sort(segments); + + // Download the segments. + ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = + new ProgressNotifier( + progressListener, + contentLength, + totalSegments, + bytesDownloaded, + segmentsDownloaded); + } byte[] buffer = new byte[BUFFER_SIZE_BYTES]; - CachingCounters cachingCounters = new CachingCounters(); for (int i = 0; i < segments.size(); i++) { - try { - CacheUtil.cache( - segments.get(i).dataSpec, - cache, - cacheKeyFactory, - dataSource, - buffer, - priorityTaskManager, - C.PRIORITY_DOWNLOAD, - cachingCounters, - isCanceled, - true); - downloadedSegments++; - } finally { - counters.newlyCachedBytes += cachingCounters.newlyCachedBytes; - updatePercentage(); + CacheUtil.cache( + segments.get(i).dataSpec, + cache, + cacheKeyFactory, + dataSource, + buffer, + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + progressNotifier, + isCanceled, + true); + if (progressNotifier != null) { + progressNotifier.onSegmentDownloaded(); } } } finally { @@ -143,26 +175,6 @@ public void cancel() { isCanceled.set(true); } - @Override - public final long getDownloadedBytes() { - return counters.totalCachedBytes(); - } - - @Override - public long getTotalBytes() { - return counters.contentLength; - } - - @Override - public final float getDownloadPercentage() { - return counters.percentage; - } - - @Override - public CachingCounters getCounters() { - return counters; - } - @Override public final void remove() throws InterruptedException { try { @@ -199,64 +211,15 @@ public final void remove() throws InterruptedException { * @param allowIncompleteList Whether to continue in the case that a load error prevents all * segments from being listed. If true then a partial segment list will be returned. If false * an {@link IOException} will be thrown. + * @return The list of downloadable {@link Segment}s. * @throws InterruptedException Thrown if the thread was interrupted. * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * the media is not in a form that allows for its segments to be listed. - * @return The list of downloadable {@link Segment}s. */ protected abstract List getSegments( DataSource dataSource, M manifest, boolean allowIncompleteList) throws InterruptedException, IOException; - /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */ - // Writes to downloadedSegments and downloadedBytes are safe. See the comment on download(). - @SuppressWarnings("NonAtomicVolatileUpdate") - private List initDownload() throws IOException, InterruptedException { - M manifest = getManifest(dataSource, manifestDataSpec); - if (!streamKeys.isEmpty()) { - manifest = manifest.copy(streamKeys); - } - List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); - CachingCounters cachingCounters = new CachingCounters(); - totalSegments = segments.size(); - downloadedSegments = 0; - counters.alreadyCachedBytes = 0; - counters.newlyCachedBytes = 0; - long totalBytes = 0; - for (int i = segments.size() - 1; i >= 0; i--) { - Segment segment = segments.get(i); - CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory, cachingCounters); - counters.alreadyCachedBytes += cachingCounters.alreadyCachedBytes; - if (cachingCounters.contentLength != C.LENGTH_UNSET) { - if (cachingCounters.alreadyCachedBytes == cachingCounters.contentLength) { - // The segment is fully downloaded. - downloadedSegments++; - segments.remove(i); - } - if (totalBytes != C.LENGTH_UNSET) { - totalBytes += cachingCounters.contentLength; - } - } else { - totalBytes = C.LENGTH_UNSET; - } - } - counters.contentLength = totalBytes; - updatePercentage(); - return segments; - } - - private void updatePercentage() { - counters.updatePercentage(); - if (counters.percentage == C.PERCENTAGE_UNSET) { - int totalSegments = this.totalSegments; - int downloadedSegments = this.downloadedSegments; - if (totalSegments != C.LENGTH_UNSET && downloadedSegments != C.LENGTH_UNSET) { - counters.percentage = - totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; - } - } - } - private void removeDataSpec(DataSpec dataSpec) { CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } @@ -269,4 +232,49 @@ protected static DataSpec getCompressibleDataSpec(Uri uri) { /* key= */ null, /* flags= */ DataSpec.FLAG_ALLOW_GZIP); } + + private static final class ProgressNotifier implements CacheUtil.ProgressListener { + + private final ProgressListener progressListener; + + private final long contentLength; + private final int totalSegments; + + private long bytesDownloaded; + private int segmentsDownloaded; + + public ProgressNotifier( + ProgressListener progressListener, + long contentLength, + int totalSegments, + long bytesDownloaded, + int segmentsDownloaded) { + this.progressListener = progressListener; + this.contentLength = contentLength; + this.totalSegments = totalSegments; + this.bytesDownloaded = bytesDownloaded; + this.segmentsDownloaded = segmentsDownloaded; + } + + @Override + public void onProgress(long requestLength, long bytesCached, long newBytesCached) { + bytesDownloaded += newBytesCached; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + public void onSegmentDownloaded() { + segmentsDownloaded++; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + private float getPercentDownloaded() { + if (contentLength != C.LENGTH_UNSET && contentLength != 0) { + return (bytesDownloaded * 100f) / contentLength; + } else if (totalSegments != 0) { + return (segmentsDownloaded * 100f) / totalSegments; + } else { + return C.PERCENTAGE_UNSET; + } + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index f715da118b0..219d736835e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -17,6 +17,7 @@ import android.net.Uri; import androidx.annotation.Nullable; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -31,36 +32,21 @@ /** * Caching related utility methods. */ -@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public final class CacheUtil { - /** Counters used during caching. */ - public static class CachingCounters { - /** The number of bytes already in the cache. */ - public volatile long alreadyCachedBytes; - /** The number of newly cached bytes. */ - public volatile long newlyCachedBytes; - /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ - public volatile long contentLength = C.LENGTH_UNSET; - /** The percentage of cached data, or {@link C#PERCENTAGE_UNSET} if unavailable. */ - public volatile float percentage; + /** Receives progress updates during cache operations. */ + public interface ProgressListener { /** - * Returns the sum of {@link #alreadyCachedBytes} and {@link #newlyCachedBytes}. + * Called when progress is made during a cache operation. + * + * @param requestLength The length of the content being cached in bytes, or {@link + * C#LENGTH_UNSET} if unknown. + * @param bytesCached The number of bytes that are cached. + * @param newBytesCached The number of bytes that have been newly cached since the last progress + * update. */ - public long totalCachedBytes() { - return alreadyCachedBytes + newlyCachedBytes; - } - - /** Updates {@link #percentage} value using other values. */ - public void updatePercentage() { - // Take local snapshot of the volatile field - long contentLength = this.contentLength; - percentage = - contentLength == C.LENGTH_UNSET - ? C.PERCENTAGE_UNSET - : ((totalCachedBytes() * 100f) / contentLength); - } + void onProgress(long requestLength, long bytesCached, long newBytesCached); } /** Default buffer size to be used while caching. */ @@ -80,48 +66,43 @@ public static String generateKey(Uri uri) { } /** - * Sets a {@link CachingCounters} to contain the number of bytes already downloaded and the length - * for the content defined by a {@code dataSpec}. {@link CachingCounters#newlyCachedBytes} is - * reset to 0. + * Queries the cache to obtain the request length and the number of bytes already cached for a + * given {@link DataSpec}. * * @param dataSpec Defines the data to be checked. * @param cache A {@link Cache} which has the data. * @param cacheKeyFactory An optional factory for cache keys. - * @param counters The {@link CachingCounters} to update. + * @return A pair containing the request length and the number of bytes that are already cached. */ - public static void getCached( - DataSpec dataSpec, - Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, - CachingCounters counters) { + public static Pair getCached( + DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long bytesLeft; + long requestLength; if (dataSpec.length != C.LENGTH_UNSET) { - bytesLeft = dataSpec.length; + requestLength = dataSpec.length; } else { long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; + requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; } - counters.contentLength = bytesLeft; - counters.alreadyCachedBytes = 0; - counters.newlyCachedBytes = 0; + long bytesAlreadyCached = 0; + long bytesLeft = requestLength; while (bytesLeft != 0) { long blockLength = cache.getCachedLength( key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); if (blockLength > 0) { - counters.alreadyCachedBytes += blockLength; + bytesAlreadyCached += blockLength; } else { blockLength = -blockLength; if (blockLength == Long.MAX_VALUE) { - return; + break; } } position += blockLength; bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; } - counters.updatePercentage(); + return Pair.create(requestLength, bytesAlreadyCached); } /** @@ -132,7 +113,7 @@ public static void getCached( * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param counters If not null, updated during caching. + * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. @@ -142,7 +123,7 @@ public static void cache( Cache cache, @Nullable CacheKeyFactory cacheKeyFactory, DataSource upstream, - @Nullable CachingCounters counters, + @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { cache( @@ -153,7 +134,7 @@ public static void cache( new byte[DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - counters, + progressListener, isCanceled, /* enableEOFException= */ false); } @@ -176,7 +157,7 @@ public static void cache( * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters If not null, updated during caching. + * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. @@ -191,19 +172,18 @@ public static void cache( byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - @Nullable CachingCounters counters, + @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, boolean enableEOFException) throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); - if (counters != null) { - // Initialize the CachingCounter values. - getCached(dataSpec, cache, cacheKeyFactory, counters); - } else { - // Dummy CachingCounters. No need to initialize as they will not be visible to the caller. - counters = new CachingCounters(); + ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = new ProgressNotifier(progressListener); + Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); + progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); } String key = buildCacheKey(dataSpec, cacheKeyFactory); @@ -234,7 +214,7 @@ public static void cache( buffer, priorityTaskManager, priority, - counters, + progressNotifier, isCanceled); if (read < blockLength) { // Reached to the end of the data. @@ -261,7 +241,7 @@ public static void cache( * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. - * @param counters Counters to be set during reading. + * @param progressNotifier A notifier through which to report progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. @@ -274,7 +254,7 @@ private static long readAndDiscard( byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters, + @Nullable ProgressNotifier progressNotifier, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -298,8 +278,8 @@ private static long readAndDiscard( dataSpec.key, dataSpec.flags); long resolvedLength = dataSource.open(dataSpec); - if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { - counters.contentLength = positionOffset + resolvedLength; + if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } long totalBytesRead = 0; while (totalBytesRead != length) { @@ -312,14 +292,15 @@ private static long readAndDiscard( ? (int) Math.min(buffer.length, length - totalBytesRead) : buffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { - if (counters.contentLength == C.LENGTH_UNSET) { - counters.contentLength = positionOffset + totalBytesRead; + if (progressNotifier != null) { + progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead); } break; } totalBytesRead += bytesRead; - counters.newlyCachedBytes += bytesRead; - counters.updatePercentage(); + if (progressNotifier != null) { + progressNotifier.onBytesCached(bytesRead); + } } return totalBytesRead; } catch (PriorityTaskManager.PriorityTooLowException exception) { @@ -374,4 +355,34 @@ private static void throwExceptionIfInterruptedOrCancelled(AtomicBoolean isCance private CacheUtil() {} + private static final class ProgressNotifier { + /** The listener to notify when progress is made. */ + private final ProgressListener listener; + /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + private long requestLength; + /** The number of bytes that are cached. */ + private long bytesCached; + + public ProgressNotifier(ProgressListener listener) { + this.listener = listener; + } + + public void init(long requestLength, long bytesCached) { + this.requestLength = requestLength; + this.bytesCached = bytesCached; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + + public void onRequestLengthResolved(long requestLength) { + if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) { + this.requestLength = requestLength; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + } + + public void onBytesCached(long newBytesCached) { + bytesCached += newBytesCached; + listener.onProgress(requestLength, bytesCached, newBytesCached); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index 73c73b6647d..f163e8d206c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -76,9 +76,9 @@ public void addAndGetDownload_existingId_returnsUpdatedDownload() throws Databas .setUri("different uri") .setCacheKey("different cacheKey") .setState(Download.STATE_FAILED) - .setDownloadPercentage(50) - .setDownloadedBytes(200) - .setTotalBytes(400) + .setPercentDownloaded(50) + .setBytesDownloaded(200) + .setContentLength(400) .setFailureReason(Download.FAILURE_REASON_UNKNOWN) .setStopReason(0x12345678) .setStartTimeMs(10) @@ -300,10 +300,10 @@ private static void assertEqual(Download download, Download that) { assertThat(download.state).isEqualTo(that.state); assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs); - assertThat(download.failureReason).isEqualTo(that.failureReason); + assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.stopReason).isEqualTo(that.stopReason); - assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); - assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); - assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); + assertThat(download.failureReason).isEqualTo(that.failureReason); + assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded()); + assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded()); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java index b5d84fa4bcd..f901b00f532 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java @@ -17,7 +17,7 @@ import android.net.Uri; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.C; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,52 +29,61 @@ * creation for tests. Tests must avoid depending on the default values but explicitly set tested * parameters during test initialization. */ -class DownloadBuilder { - private final CachingCounters counters; +/* package */ final class DownloadBuilder { + + private final DownloadProgress progress; + private String id; private String type; private Uri uri; + private List streamKeys; @Nullable private String cacheKey; + private byte[] customMetadata; + private int state; - private int failureReason; - private int stopReason; private long startTimeMs; private long updateTimeMs; - private List streamKeys; - private byte[] customMetadata; + private long contentLength; + private int stopReason; + private int failureReason; - DownloadBuilder(String id) { - this(id, "type", Uri.parse("uri"), /* cacheKey= */ null, new byte[0], Collections.emptyList()); + /* package */ DownloadBuilder(String id) { + this( + id, + "type", + Uri.parse("uri"), + /* streamKeys= */ Collections.emptyList(), + /* cacheKey= */ null, + new byte[0]); } - DownloadBuilder(DownloadRequest request) { + /* package */ DownloadBuilder(DownloadRequest request) { this( request.id, request.type, request.uri, + request.streamKeys, request.customCacheKey, - request.data, - request.streamKeys); + request.data); } - DownloadBuilder( + /* package */ DownloadBuilder( String id, String type, Uri uri, + List streamKeys, String cacheKey, - byte[] customMetadata, - List streamKeys) { + byte[] customMetadata) { this.id = id; this.type = type; this.uri = uri; + this.streamKeys = streamKeys; this.cacheKey = cacheKey; + this.customMetadata = customMetadata; this.state = Download.STATE_QUEUED; + this.contentLength = C.LENGTH_UNSET; this.failureReason = Download.FAILURE_REASON_NONE; - this.startTimeMs = (long) 0; - this.updateTimeMs = (long) 0; - this.streamKeys = streamKeys; - this.customMetadata = customMetadata; - this.counters = new CachingCounters(); + this.progress = new DownloadProgress(); } public DownloadBuilder setId(String id) { @@ -107,18 +116,18 @@ public DownloadBuilder setState(int state) { return this; } - public DownloadBuilder setDownloadPercentage(float downloadPercentage) { - counters.percentage = downloadPercentage; + public DownloadBuilder setPercentDownloaded(float percentDownloaded) { + progress.percentDownloaded = percentDownloaded; return this; } - public DownloadBuilder setDownloadedBytes(long downloadedBytes) { - counters.alreadyCachedBytes = downloadedBytes; + public DownloadBuilder setBytesDownloaded(long bytesDownloaded) { + progress.bytesDownloaded = bytesDownloaded; return this; } - public DownloadBuilder setTotalBytes(long totalBytes) { - counters.contentLength = totalBytes; + public DownloadBuilder setContentLength(long contentLength) { + this.contentLength = contentLength; return this; } @@ -156,6 +165,13 @@ public Download build() { DownloadRequest request = new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata); return new Download( - request, state, failureReason, stopReason, startTimeMs, updateTimeMs, counters); + request, + state, + startTimeMs, + updateTimeMs, + contentLength, + stopReason, + failureReason, + progress); } } 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 17328248c64..5798e9df8c3 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 @@ -20,6 +20,7 @@ import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; @@ -27,7 +28,6 @@ import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -184,7 +184,7 @@ public void downloadProgressOnRetry_retryCountResets() throws Throwable { int tooManyRetries = MIN_RETRY_COUNT + 10; for (int i = 0; i < tooManyRetries; i++) { - downloader.increaseDownloadedByteCount(); + downloader.incrementBytesDownloaded(); downloader.assertStarted(MAX_RETRY_DELAY).fail(); } downloader.assertStarted(MAX_RETRY_DELAY).unblock(); @@ -555,11 +555,11 @@ private void runOnMainThread(TestRunnable r) { private static void assertEqualIgnoringTimeFields(Download download, Download that) { assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); + assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.failureReason).isEqualTo(that.failureReason); assertThat(download.stopReason).isEqualTo(that.stopReason); - assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); - assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); - assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); + assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded()); + assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded()); } private static DownloadRequest createDownloadRequest() { @@ -722,21 +722,23 @@ private static final class FakeDownloader implements Downloader { private volatile boolean cancelled; private volatile boolean enableDownloadIOException; private volatile int startCount; - private CachingCounters counters; + private volatile int bytesDownloaded; private FakeDownloader() { this.started = new CountDownLatch(1); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); - counters = new CachingCounters(); } @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @Override - public void download() throws InterruptedException, IOException { + public void download(ProgressListener listener) throws InterruptedException, IOException { // It's ok to update this directly as no other thread will update it. startCount++; started.countDown(); block(); + if (bytesDownloaded > 0) { + listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET); + } if (enableDownloadIOException) { enableDownloadIOException = false; throw new IOException(); @@ -783,7 +785,7 @@ private FakeDownloader assertStarted(int timeout) throws InterruptedException { return this; } - private FakeDownloader assertStartCount(int count) throws InterruptedException { + private FakeDownloader assertStartCount(int count) { assertThat(startCount).isEqualTo(count); return this; } @@ -823,34 +825,14 @@ private FakeDownloader fail() { return unblock(); } - @Override - public long getDownloadedBytes() { - return counters.newlyCachedBytes; - } - - @Override - public long getTotalBytes() { - return counters.contentLength; - } - - @Override - public float getDownloadPercentage() { - return counters.percentage; - } - - @Override - public CachingCounters getCounters() { - return counters; - } - private void assertDoesNotStart() throws InterruptedException { Thread.sleep(ASSERT_FALSE_TIME); assertThat(started.getCount()).isEqualTo(1); } @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) - private void increaseDownloadedByteCount() { - counters.newlyCachedBytes++; + private void incrementBytesDownloaded() { + bytesDownloaded++; } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 4005edc3a6f..956a5fc2839 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -343,7 +343,7 @@ public void testSwitchToCacheSourceWithReadOnlyCacheDataSource() throws Exceptio cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -392,7 +392,7 @@ public void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Excep cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -416,7 +416,7 @@ public void testDeleteCachedWhileReadingFromUpstreamWithReadOnlyCacheDataSourceD cache, /* cacheKeyFactory= */ null, upstream, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. @@ -452,7 +452,7 @@ public void testDeleteCachedWhileReadingFromUpstreamWithBlockingCacheDataSourceD cache, /* cacheKeyFactory= */ null, upstream, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Create blocking CacheDataSource. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index ba06862385b..9a449b2ebd4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.fail; import android.net.Uri; +import android.util.Pair; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -30,7 +31,6 @@ import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.File; @@ -100,12 +100,12 @@ public void setUp() throws Exception { } @After - public void tearDown() throws Exception { + public void tearDown() { Util.recursiveDelete(tempFolder); } @Test - public void testGenerateKey() throws Exception { + public void testGenerateKey() { assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull(); Uri testUri = Uri.parse("test"); @@ -120,7 +120,7 @@ public void testGenerateKey() throws Exception { } @Test - public void testDefaultCacheKeyFactory_buildCacheKey() throws Exception { + public void testDefaultCacheKeyFactory_buildCacheKey() { Uri testUri = Uri.parse("test"); String key = "key"; // If DataSpec.key is present, returns it. @@ -136,62 +136,66 @@ public void testDefaultCacheKeyFactory_buildCacheKey() throws Exception { } @Test - public void testGetCachedNoData() throws Exception { - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + public void testGetCachedNoData() { + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 0, 0, C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.second).isEqualTo(0); } @Test - public void testGetCachedDataUnknownLength() throws Exception { + public void testGetCachedDataUnknownLength() { // Mock there is 100 bytes cached at the beginning mockCache.spansAndGaps = new int[] {100}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 100, 0, C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.second).isEqualTo(100); } @Test - public void testGetCachedNoDataKnownLength() throws Exception { + public void testGetCachedNoDataKnownLength() { mockCache.contentLength = 1000; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 0, 0, 1000); + assertThat(contentLengthAndBytesCached.first).isEqualTo(1000); + assertThat(contentLengthAndBytesCached.second).isEqualTo(0); } @Test - public void testGetCached() throws Exception { + public void testGetCached() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 300, 0, 1000); + assertThat(contentLengthAndBytesCached.first).isEqualTo(1000); + assertThat(contentLengthAndBytesCached.second).isEqualTo(300); } @Test - public void testGetCachedFromNonZeroPosition() throws Exception { + public void testGetCachedFromNonZeroPosition() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec( - Uri.parse("test"), - /* absoluteStreamPosition= */ 100, - /* length= */ C.LENGTH_UNSET, - /* key= */ null), - mockCache, - /* cacheKeyFactory= */ null, - counters); - - assertCounters(counters, 200, 0, 900); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec( + Uri.parse("test"), + /* absoluteStreamPosition= */ 100, + /* length= */ C.LENGTH_UNSET, + /* key= */ null), + mockCache, + /* cacheKeyFactory= */ null); + + assertThat(contentLengthAndBytesCached.first).isEqualTo(900); + assertThat(contentLengthAndBytesCached.second).isEqualTo(200); } @Test @@ -208,7 +212,7 @@ public void testCache() throws Exception { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 100); + counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @@ -223,7 +227,8 @@ public void testCacheSetOffsetAndLength() throws Exception { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 20, 20); + counters.assertValues(0, 20, 20); + counters.reset(); CacheUtil.cache( new DataSpec(testUri), @@ -233,7 +238,7 @@ public void testCacheSetOffsetAndLength() throws Exception { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -249,7 +254,7 @@ public void testCacheUnknownLength() throws Exception { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 100); + counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @@ -266,7 +271,8 @@ public void testCacheUnknownLengthPartialCaching() throws Exception { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 20, 20); + counters.assertValues(0, 20, 20); + counters.reset(); CacheUtil.cache( new DataSpec(testUri), @@ -276,7 +282,7 @@ public void testCacheUnknownLengthPartialCaching() throws Exception { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -291,7 +297,7 @@ public void testCacheLengthExceedsActualDataLength() throws Exception { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 1000); + counters.assertValues(0, 100, 1000); assertCachedData(cache, fakeDataSet); } @@ -312,7 +318,7 @@ public void testCacheThrowEOFException() throws Exception { new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null, /* enableEOFException= */ true); fail(); @@ -328,9 +334,9 @@ public void testCachePolling() throws Exception { new FakeDataSet() .newData("test_data") .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(() -> assertCounters(counters, 0, 100, 300)) + .appendReadAction(() -> counters.assertValues(0, 100, 300)) .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(() -> assertCounters(counters, 0, 200, 300)) + .appendReadAction(() -> counters.assertValues(0, 200, 300)) .appendReadData(TestUtil.buildTestData(100)) .endData(); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -343,7 +349,7 @@ public void testCachePolling() throws Exception { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 300, 300); + counters.assertValues(0, 300, 300); assertCachedData(cache, fakeDataSet); } @@ -369,7 +375,7 @@ public void testRemove() throws Exception { new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null, true); CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null); @@ -377,10 +383,34 @@ public void testRemove() throws Exception { assertCacheEmpty(cache); } - private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, - int newlyCachedBytes, int contentLength) { - assertThat(counters.alreadyCachedBytes).isEqualTo(alreadyCachedBytes); - assertThat(counters.newlyCachedBytes).isEqualTo(newlyCachedBytes); - assertThat(counters.contentLength).isEqualTo(contentLength); + private static final class CachingCounters implements CacheUtil.ProgressListener { + + private long contentLength = C.LENGTH_UNSET; + private long bytesAlreadyCached; + private long bytesNewlyCached; + private boolean seenFirstProgressUpdate; + + @Override + public void onProgress(long contentLength, long bytesCached, long newBytesCached) { + this.contentLength = contentLength; + if (!seenFirstProgressUpdate) { + bytesAlreadyCached = bytesCached; + seenFirstProgressUpdate = true; + } + bytesNewlyCached = bytesCached - bytesAlreadyCached; + } + + public void assertValues(int bytesAlreadyCached, int bytesNewlyCached, int contentLength) { + assertThat(this.bytesAlreadyCached).isEqualTo(bytesAlreadyCached); + assertThat(this.bytesNewlyCached).isEqualTo(bytesNewlyCached); + assertThat(this.contentLength).isEqualTo(contentLength); + } + + public void reset() { + contentLength = C.LENGTH_UNSET; + bytesAlreadyCached = 0; + bytesNewlyCached = 0; + seenFirstProgressUpdate = false; + } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 5636c734914..2754a3341ac 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -45,7 +45,7 @@ *

    Example usage: * *

    {@code
    - * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
    + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
      * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      * DownloaderConstructorHelper constructorHelper =
      *     new DownloaderConstructorHelper(cache, factory);
    @@ -55,7 +55,7 @@
      *     new DashDownloader(
      *         manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);
      * // Perform the download.
    - * dashDownloader.download();
    + * dashDownloader.download(progressListener);
      * // Access downloaded data using CacheDataSource
      * CacheDataSource cacheDataSource =
      *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
    diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
    index 9eacd28f8d4..b3a6b8271bf 100644
    --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
    +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
    @@ -62,6 +62,7 @@ public class DashDownloaderTest {
     
       private SimpleCache cache;
       private File tempFolder;
    +  private ProgressListener progressListener;
     
       @Before
       public void setUp() throws Exception {
    @@ -69,6 +70,7 @@ public void setUp() throws Exception {
         tempFolder =
             Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
         cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
    +    progressListener = new ProgressListener();
       }
     
       @After
    @@ -77,7 +79,7 @@ public void tearDown() {
       }
     
       @Test
    -  public void testCreateWithDefaultDownloaderFactory() throws Exception {
    +  public void testCreateWithDefaultDownloaderFactory() {
         DownloaderConstructorHelper constructorHelper =
             new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
         DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
    @@ -105,7 +107,7 @@ public void testDownloadRepresentation() throws Exception {
                 .setRandomData("audio_segment_3", 6);
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -124,7 +126,7 @@ public void testDownloadRepresentationInSmallParts() throws Exception {
                 .setRandomData("audio_segment_3", 6);
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -143,7 +145,7 @@ public void testDownloadRepresentations() throws Exception {
     
         DashDownloader dashDownloader =
             getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -164,7 +166,7 @@ public void testDownloadAllRepresentations() throws Exception {
                 .setRandomData("period_2_segment_3", 3);
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -186,7 +188,7 @@ public void testProgressiveDownload() throws Exception {
     
         DashDownloader dashDownloader =
             getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
     
         DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
         assertThat(openedDataSpecs.length).isEqualTo(8);
    @@ -218,7 +220,7 @@ public void testProgressiveDownloadSeparatePeriods() throws Exception {
     
         DashDownloader dashDownloader =
             getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
     
         DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
         assertThat(openedDataSpecs.length).isEqualTo(8);
    @@ -248,12 +250,12 @@ public void testDownloadRepresentationFailure() throws Exception {
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
         try {
    -      dashDownloader.download();
    +      dashDownloader.download(progressListener);
           fail();
         } catch (IOException e) {
           // Expected.
         }
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -272,18 +274,17 @@ public void testCounters() throws Exception {
                 .setRandomData("audio_segment_3", 6);
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
    -    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0);
     
         try {
    -      dashDownloader.download();
    +      dashDownloader.download(progressListener);
           fail();
         } catch (IOException e) {
           // Failure expected after downloading init data, segment 1 and 2 bytes in segment 2.
         }
    -    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 2);
    +    progressListener.assertBytesDownloaded(10 + 4 + 2);
     
    -    dashDownloader.download();
    -    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6);
    +    dashDownloader.download(progressListener);
    +    progressListener.assertBytesDownloaded(10 + 4 + 5 + 6);
       }
     
       @Test
    @@ -301,7 +302,7 @@ public void testRemove() throws Exception {
     
         DashDownloader dashDownloader =
             getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         dashDownloader.remove();
         assertCacheEmpty(cache);
       }
    @@ -315,7 +316,7 @@ public void testRepresentationWithoutIndex() throws Exception {
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
         try {
    -      dashDownloader.download();
    +      dashDownloader.download(progressListener);
           fail();
         } catch (DownloadException e) {
           // Expected.
    @@ -339,4 +340,17 @@ private static ArrayList keysList(StreamKey... keys) {
         return keysList;
       }
     
    +  private static final class ProgressListener implements Downloader.ProgressListener {
    +
    +    private long bytesDownloaded;
    +
    +    @Override
    +    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
    +      this.bytesDownloaded = bytesDownloaded;
    +    }
    +
    +    public void assertBytesDownloaded(long bytesDownloaded) {
    +      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);
    +    }
    +  }
     }
    diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
    index 8e744f9a775..6e6d0afd493 100644
    --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
    +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
    @@ -39,7 +39,7 @@
      * 

    Example usage: * *

    {@code
    - * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
    + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
      * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      * DownloaderConstructorHelper constructorHelper =
      *     new DownloaderConstructorHelper(cache, factory);
    @@ -50,7 +50,7 @@
      *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
      *         constructorHelper);
      * // Perform the download.
    - * hlsDownloader.download();
    + * hlsDownloader.download(progressListener);
      * // Access downloaded data using CacheDataSource
      * CacheDataSource cacheDataSource =
      *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
    diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
    index b92953c3b55..7d77a78316e 100644
    --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
    +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
    @@ -67,6 +67,7 @@ public class HlsDownloaderTest {
     
       private SimpleCache cache;
       private File tempFolder;
    +  private ProgressListener progressListener;
       private FakeDataSet fakeDataSet;
     
       @Before
    @@ -74,7 +75,7 @@ public void setUp() throws Exception {
         tempFolder =
             Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
         cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
    -
    +    progressListener = new ProgressListener();
         fakeDataSet =
             new FakeDataSet()
                 .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
    @@ -94,7 +95,7 @@ public void tearDown() {
       }
     
       @Test
    -  public void testCreateWithDefaultDownloaderFactory() throws Exception {
    +  public void testCreateWithDefaultDownloaderFactory() {
         DownloaderConstructorHelper constructorHelper =
             new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
         DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
    @@ -115,17 +116,16 @@ public void testCreateWithDefaultDownloaderFactory() throws Exception {
       public void testCounterMethods() throws Exception {
         HlsDownloader downloader =
             getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
    -    downloader.download();
    +    downloader.download(progressListener);
     
    -    assertThat(downloader.getDownloadedBytes())
    -        .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
    +    progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
       }
     
       @Test
       public void testDownloadRepresentation() throws Exception {
         HlsDownloader downloader =
             getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
    -    downloader.download();
    +    downloader.download(progressListener);
     
         assertCachedData(
             cache,
    @@ -143,7 +143,7 @@ public void testDownloadMultipleRepresentations() throws Exception {
             getHlsDownloader(
                 MASTER_PLAYLIST_URI,
                 getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
    -    downloader.download();
    +    downloader.download(progressListener);
     
         assertCachedData(cache, fakeDataSet);
       }
    @@ -162,7 +162,7 @@ public void testDownloadAllRepresentations() throws Exception {
             .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15);
     
         HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys());
    -    downloader.download();
    +    downloader.download(progressListener);
     
         assertCachedData(cache, fakeDataSet);
       }
    @@ -173,7 +173,7 @@ public void testRemove() throws Exception {
             getHlsDownloader(
                 MASTER_PLAYLIST_URI,
                 getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
    -    downloader.download();
    +    downloader.download(progressListener);
         downloader.remove();
     
         assertCacheEmpty(cache);
    @@ -182,7 +182,7 @@ public void testRemove() throws Exception {
       @Test
       public void testDownloadMediaPlaylist() throws Exception {
         HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys());
    -    downloader.download();
    +    downloader.download(progressListener);
     
         assertCachedData(
             cache,
    @@ -205,7 +205,7 @@ public void testDownloadEncMediaPlaylist() throws Exception {
                 .setRandomData("fileSequence2.ts", 12);
     
         HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys());
    -    downloader.download();
    +    downloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -222,4 +222,18 @@ private static ArrayList getKeys(int... variantIndices) {
         }
         return streamKeys;
       }
    +
    +  private static final class ProgressListener implements Downloader.ProgressListener {
    +
    +    private long bytesDownloaded;
    +
    +    @Override
    +    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
    +      this.bytesDownloaded = bytesDownloaded;
    +    }
    +
    +    public void assertBytesDownloaded(long bytesDownloaded) {
    +      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);
    +    }
    +  }
     }
    diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
    index 18820ca49cb..1331fe46178 100644
    --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
    +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
    @@ -37,7 +37,7 @@
      * 

    Example usage: * *

    {@code
    - * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
    + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
      * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      * DownloaderConstructorHelper constructorHelper =
      *     new DownloaderConstructorHelper(cache, factory);
    @@ -48,7 +48,7 @@
      *         Collections.singletonList(new StreamKey(0, 0)),
      *         constructorHelper);
      * // Perform the download.
    - * ssDownloader.download();
    + * ssDownloader.download(progressListener);
      * // Access downloaded data using CacheDataSource
      * CacheDataSource cacheDataSource =
      *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
    diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
    index b26b8eaac44..178cd44dd30 100644
    --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
    +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
    @@ -75,12 +75,12 @@ public Notification buildProgressNotification(
             continue;
           }
           haveDownloadTasks = true;
    -      float downloadPercentage = download.getDownloadPercentage();
    +      float downloadPercentage = download.getPercentDownloaded();
           if (downloadPercentage != C.PERCENTAGE_UNSET) {
             allDownloadPercentagesUnknown = false;
             totalPercentage += downloadPercentage;
           }
    -      haveDownloadedBytes |= download.getDownloadedBytes() > 0;
    +      haveDownloadedBytes |= download.getBytesDownloaded() > 0;
           downloadTaskCount++;
         }
     
    diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
    index 67c840e6816..f5af2472c9a 100644
    --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
    +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
    @@ -89,7 +89,7 @@ public void tearDown() {
       @Test
       public void testDownload() throws Exception {
         DashDownloader dashDownloader = downloadContent();
    -    dashDownloader.download();
    +    dashDownloader.download(/* progressListener= */ null);
     
         testRunner
             .setStreamName("test_h264_fixed_download")
    
    From b30efe968b8459d77779daea9960d15c3e6268a1 Mon Sep 17 00:00:00 2001
    From: olly 
    Date: Thu, 18 Apr 2019 23:08:43 +0100
    Subject: [PATCH 021/219] Clean up database tables for launch
    
    PiperOrigin-RevId: 244267255
    ---
     .../offline/DefaultDownloadIndex.java         | 127 ++++++++----------
     .../cache/CacheFileMetadataIndex.java         |   5 +-
     .../upstream/cache/CachedContentIndex.java    |  13 +-
     .../offline/DefaultDownloadIndexTest.java     |   2 +-
     4 files changed, 60 insertions(+), 87 deletions(-)
    
    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 6838c246283..252c058b88e 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
    @@ -23,7 +23,6 @@
     import android.net.Uri;
     import androidx.annotation.Nullable;
     import androidx.annotation.VisibleForTesting;
    -import android.text.TextUtils;
     import com.google.android.exoplayer2.database.DatabaseIOException;
     import com.google.android.exoplayer2.database.DatabaseProvider;
     import com.google.android.exoplayer2.database.VersionTable;
    @@ -37,32 +36,22 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
     
       private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "Downloads";
     
    -  @VisibleForTesting /* package */ static final int TABLE_VERSION = 1;
    +  @VisibleForTesting /* package */ static final int TABLE_VERSION = 2;
     
       private static final String COLUMN_ID = "id";
       private static final String COLUMN_TYPE = "title";
    -  private static final String COLUMN_URI = "subtitle";
    +  private static final String COLUMN_URI = "uri";
       private static final String COLUMN_STREAM_KEYS = "stream_keys";
    -  private static final String COLUMN_CUSTOM_CACHE_KEY = "cache_key";
    -  private static final String COLUMN_DATA = "custom_metadata";
    +  private static final String COLUMN_CUSTOM_CACHE_KEY = "custom_cache_key";
    +  private static final String COLUMN_DATA = "data";
       private static final String COLUMN_STATE = "state";
    -  private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage";
    -  private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes";
    -  private static final String COLUMN_TOTAL_BYTES = "total_bytes";
    -  private static final String COLUMN_FAILURE_REASON = "failure_reason";
    -  private static final String COLUMN_STOP_REASON = "manual_stop_reason";
       private static final String COLUMN_START_TIME_MS = "start_time_ms";
       private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms";
    -
    -  /** @deprecated No longer used. */
    -  @SuppressWarnings("DeprecatedIsStillUsed")
    -  @Deprecated
    -  private static final String COLUMN_STOP_FLAGS = "stop_flags";
    -
    -  /** @deprecated No longer used. */
    -  @SuppressWarnings("DeprecatedIsStillUsed")
    -  @Deprecated
    -  private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements";
    +  private static final String COLUMN_CONTENT_LENGTH = "content_length";
    +  private static final String COLUMN_STOP_REASON = "stop_reason";
    +  private static final String COLUMN_FAILURE_REASON = "failure_reason";
    +  private static final String COLUMN_PERCENT_DOWNLOADED = "percent_downloaded";
    +  private static final String COLUMN_BYTES_DOWNLOADED = "bytes_downloaded";
     
       private static final int COLUMN_INDEX_ID = 0;
       private static final int COLUMN_INDEX_TYPE = 1;
    @@ -71,13 +60,13 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
       private static final int COLUMN_INDEX_CUSTOM_CACHE_KEY = 4;
       private static final int COLUMN_INDEX_DATA = 5;
       private static final int COLUMN_INDEX_STATE = 6;
    -  private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 7;
    -  private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 8;
    -  private static final int COLUMN_INDEX_TOTAL_BYTES = 9;
    -  private static final int COLUMN_INDEX_FAILURE_REASON = 10;
    -  private static final int COLUMN_INDEX_STOP_REASON = 11;
    -  private static final int COLUMN_INDEX_START_TIME_MS = 12;
    -  private static final int COLUMN_INDEX_UPDATE_TIME_MS = 13;
    +  private static final int COLUMN_INDEX_START_TIME_MS = 7;
    +  private static final int COLUMN_INDEX_UPDATE_TIME_MS = 8;
    +  private static final int COLUMN_INDEX_CONTENT_LENGTH = 9;
    +  private static final int COLUMN_INDEX_STOP_REASON = 10;
    +  private static final int COLUMN_INDEX_FAILURE_REASON = 11;
    +  private static final int COLUMN_INDEX_PERCENT_DOWNLOADED = 12;
    +  private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13;
     
       private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?";
       private static final String WHERE_STATE_TERMINAL =
    @@ -92,13 +81,13 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
             COLUMN_CUSTOM_CACHE_KEY,
             COLUMN_DATA,
             COLUMN_STATE,
    -        COLUMN_DOWNLOAD_PERCENTAGE,
    -        COLUMN_DOWNLOADED_BYTES,
    -        COLUMN_TOTAL_BYTES,
    -        COLUMN_FAILURE_REASON,
    -        COLUMN_STOP_REASON,
             COLUMN_START_TIME_MS,
    -        COLUMN_UPDATE_TIME_MS
    +        COLUMN_UPDATE_TIME_MS,
    +        COLUMN_CONTENT_LENGTH,
    +        COLUMN_STOP_REASON,
    +        COLUMN_FAILURE_REASON,
    +        COLUMN_PERCENT_DOWNLOADED,
    +        COLUMN_BYTES_DOWNLOADED,
           };
     
       private static final String TABLE_SCHEMA =
    @@ -109,32 +98,28 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
               + " TEXT NOT NULL,"
               + COLUMN_URI
               + " TEXT NOT NULL,"
    +          + COLUMN_STREAM_KEYS
    +          + " TEXT NOT NULL,"
               + COLUMN_CUSTOM_CACHE_KEY
               + " TEXT,"
    +          + COLUMN_DATA
    +          + " BLOB NOT NULL,"
               + COLUMN_STATE
               + " INTEGER NOT NULL,"
    -          + COLUMN_DOWNLOAD_PERCENTAGE
    -          + " REAL NOT NULL,"
    -          + COLUMN_DOWNLOADED_BYTES
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_TOTAL_BYTES
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_FAILURE_REASON
    +          + COLUMN_START_TIME_MS
               + " INTEGER NOT NULL,"
    -          + COLUMN_STOP_FLAGS
    +          + COLUMN_UPDATE_TIME_MS
               + " INTEGER NOT NULL,"
    -          + COLUMN_NOT_MET_REQUIREMENTS
    +          + COLUMN_CONTENT_LENGTH
               + " INTEGER NOT NULL,"
               + COLUMN_STOP_REASON
               + " INTEGER NOT NULL,"
    -          + COLUMN_START_TIME_MS
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_UPDATE_TIME_MS
    +          + COLUMN_FAILURE_REASON
               + " INTEGER NOT NULL,"
    -          + COLUMN_STREAM_KEYS
    -          + " TEXT NOT NULL,"
    -          + COLUMN_DATA
    -          + " BLOB NOT NULL)";
    +          + COLUMN_PERCENT_DOWNLOADED
    +          + " REAL NOT NULL,"
    +          + COLUMN_BYTES_DOWNLOADED
    +          + " INTEGER NOT NULL)";
     
       private static final String TRUE = "1";
     
    @@ -170,8 +155,7 @@ public DefaultDownloadIndex(DatabaseProvider databaseProvider) {
        *     tables in which downloads are persisted.
        */
       public DefaultDownloadIndex(DatabaseProvider databaseProvider, String name) {
    -    // TODO: Remove this backward compatibility hack for launch.
    -    this.name = TextUtils.isEmpty(name) ? "singleton" : name;
    +    this.name = name;
         this.databaseProvider = databaseProvider;
         tableName = TABLE_PREFIX + name;
       }
    @@ -211,13 +195,11 @@ public void putDownload(Download download) throws DatabaseIOException {
         values.put(COLUMN_STATE, download.state);
         values.put(COLUMN_START_TIME_MS, download.startTimeMs);
         values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs);
    -    values.put(COLUMN_TOTAL_BYTES, download.contentLength);
    +    values.put(COLUMN_CONTENT_LENGTH, download.contentLength);
         values.put(COLUMN_STOP_REASON, download.stopReason);
         values.put(COLUMN_FAILURE_REASON, download.failureReason);
    -    values.put(COLUMN_DOWNLOAD_PERCENTAGE, download.getPercentDownloaded());
    -    values.put(COLUMN_DOWNLOADED_BYTES, download.getBytesDownloaded());
    -    values.put(COLUMN_STOP_FLAGS, 0);
    -    values.put(COLUMN_NOT_MET_REQUIREMENTS, 0);
    +    values.put(COLUMN_PERCENT_DOWNLOADED, download.getPercentDownloaded());
    +    values.put(COLUMN_BYTES_DOWNLOADED, download.getBytesDownloaded());
         try {
           SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
           writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
    @@ -270,7 +252,7 @@ private void ensureInitialized() throws DatabaseIOException {
         try {
           SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
           int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name);
    -      if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
    +      if (version != TABLE_VERSION) {
             SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
             writableDatabase.beginTransaction();
             try {
    @@ -282,9 +264,6 @@ private void ensureInitialized() throws DatabaseIOException {
             } finally {
               writableDatabase.endTransaction();
             }
    -      } else if (version < TABLE_VERSION) {
    -        // There is no previous version currently.
    -        throw new IllegalStateException();
           }
           initialized = true;
         } catch (SQLException e) {
    @@ -330,23 +309,23 @@ private static String getStateQuery(@Download.State int... states) {
       private static Download getDownloadForCurrentRow(Cursor cursor) {
         DownloadRequest request =
             new DownloadRequest(
    -            cursor.getString(COLUMN_INDEX_ID),
    -            cursor.getString(COLUMN_INDEX_TYPE),
    -            Uri.parse(cursor.getString(COLUMN_INDEX_URI)),
    -            decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
    -            cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY),
    -            cursor.getBlob(COLUMN_INDEX_DATA));
    +            /* id= */ cursor.getString(COLUMN_INDEX_ID),
    +            /* type= */ cursor.getString(COLUMN_INDEX_TYPE),
    +            /* uri= */ Uri.parse(cursor.getString(COLUMN_INDEX_URI)),
    +            /* streamKeys= */ decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
    +            /* customCacheKey= */ cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY),
    +            /* data= */ cursor.getBlob(COLUMN_INDEX_DATA));
         DownloadProgress downloadProgress = new DownloadProgress();
    -    downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES);
    -    downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE);
    +    downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED);
    +    downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED);
         return new Download(
             request,
    -        cursor.getInt(COLUMN_INDEX_STATE),
    -        cursor.getLong(COLUMN_INDEX_START_TIME_MS),
    -        cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
    -        cursor.getLong(COLUMN_INDEX_TOTAL_BYTES),
    -        cursor.getInt(COLUMN_INDEX_STOP_REASON),
    -        cursor.getInt(COLUMN_INDEX_FAILURE_REASON),
    +        /* state= */ cursor.getInt(COLUMN_INDEX_STATE),
    +        /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS),
    +        /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
    +        /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH),
    +        /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON),
    +        /* failureReason= */ cursor.getInt(COLUMN_INDEX_FAILURE_REASON),
             downloadProgress);
       }
     
    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
    index 027172e090d..2a8b393ed3a 100644
    --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
    +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
    @@ -107,7 +107,7 @@ public void initialize(long uid) throws DatabaseIOException {
           int version =
               VersionTable.getVersion(
                   readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
    -      if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
    +      if (version != TABLE_VERSION) {
             SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
             writableDatabase.beginTransaction();
             try {
    @@ -119,9 +119,6 @@ public void initialize(long uid) throws DatabaseIOException {
             } finally {
               writableDatabase.endTransaction();
             }
    -      } else if (version < TABLE_VERSION) {
    -        // There is no previous version currently.
    -        throw new IllegalStateException();
           }
         } catch (SQLException e) {
           throw new DatabaseIOException(e);
    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
    index 8fa04e53381..20a80a1a359 100644
    --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
    +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
    @@ -63,12 +63,8 @@
     
       /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi";
     
    -  private static final int VERSION = 2;
    -  private static final int VERSION_METADATA_INTRODUCED = 2;
       private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;
     
    -  private static final int FLAG_ENCRYPTED_INDEX = 1;
    -
       private final HashMap keyToContent;
       /**
        * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that
    @@ -464,6 +460,10 @@ void load(HashMap content, SparseArray<@NullableType Stri
       /** {@link Storage} implementation that uses an {@link AtomicFile}. */
       private static class LegacyStorage implements Storage {
     
    +    private static final int VERSION = 2;
    +    private static final int VERSION_METADATA_INTRODUCED = 2;
    +    private static final int FLAG_ENCRYPTED_INDEX = 1;
    +
         private final boolean encrypt;
         @Nullable private final Cipher cipher;
         @Nullable private final SecretKeySpec secretKeySpec;
    @@ -770,7 +770,7 @@ public void load(
                     databaseProvider.getReadableDatabase(),
                     VersionTable.FEATURE_CACHE_CONTENT_METADATA,
                     hexUid);
    -        if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
    +        if (version != TABLE_VERSION) {
               SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
               writableDatabase.beginTransaction();
               try {
    @@ -779,9 +779,6 @@ public void load(
               } finally {
                 writableDatabase.endTransaction();
               }
    -        } else if (version < TABLE_VERSION) {
    -          // There is no previous version currently.
    -          throw new IllegalStateException();
             }
     
             try (Cursor cursor = getCursor()) {
    diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java
    index f163e8d206c..f42a1c6086c 100644
    --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java
    +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java
    @@ -32,7 +32,7 @@
     @RunWith(AndroidJUnit4.class)
     public class DefaultDownloadIndexTest {
     
    -  private static final String EMPTY_NAME = "singleton";
    +  private static final String EMPTY_NAME = "";
     
       private ExoDatabaseProvider databaseProvider;
       private DefaultDownloadIndex downloadIndex;
    
    From b8cdd7e40bff8f56aa078a8e2fd760e21cedec39 Mon Sep 17 00:00:00 2001
    From: olly 
    Date: Thu, 18 Apr 2019 23:17:03 +0100
    Subject: [PATCH 022/219] Fix lint warnings for 2.10
    
    PiperOrigin-RevId: 244268855
    ---
     .../mediasession/MediaSessionConnector.java   | 21 ++++++++++++++-----
     .../exoplayer2/ExoPlaybackException.java      |  3 ---
     2 files changed, 16 insertions(+), 8 deletions(-)
    
    diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
    index 35990573ad7..24cf4062f76 100644
    --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
    +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
    @@ -1089,17 +1089,26 @@ public void onStop() {
         }
     
         @Override
    -    public void onSetShuffleMode(int shuffleMode) {
    +    public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
           if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
    -        boolean shuffleModeEnabled =
    -            shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
    -                || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP;
    +        boolean shuffleModeEnabled;
    +        switch (shuffleMode) {
    +          case PlaybackStateCompat.SHUFFLE_MODE_ALL:
    +          case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
    +            shuffleModeEnabled = true;
    +            break;
    +          case PlaybackStateCompat.SHUFFLE_MODE_NONE:
    +          case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
    +          default:
    +            shuffleModeEnabled = false;
    +            break;
    +        }
             controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);
           }
         }
     
         @Override
    -    public void onSetRepeatMode(int mediaSessionRepeatMode) {
    +    public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
           if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
             @RepeatModeUtil.RepeatToggleModes int repeatMode;
             switch (mediaSessionRepeatMode) {
    @@ -1110,6 +1119,8 @@ public void onSetRepeatMode(int mediaSessionRepeatMode) {
               case PlaybackStateCompat.REPEAT_MODE_ONE:
                 repeatMode = Player.REPEAT_MODE_ONE;
                 break;
    +          case PlaybackStateCompat.REPEAT_MODE_NONE:
    +          case PlaybackStateCompat.REPEAT_MODE_INVALID:
               default:
                 repeatMode = Player.REPEAT_MODE_OFF;
                 break;
    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
    index 4a8f8709e9d..b5f8f954bbb 100644
    --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
    +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
    @@ -17,7 +17,6 @@
     
     import androidx.annotation.IntDef;
     import androidx.annotation.Nullable;
    -import androidx.annotation.VisibleForTesting;
     import com.google.android.exoplayer2.source.MediaSource;
     import com.google.android.exoplayer2.util.Assertions;
     import java.io.IOException;
    @@ -103,7 +102,6 @@ public static ExoPlaybackException createForRenderer(Exception cause, int render
        * @param cause The cause of the failure.
        * @return The created instance.
        */
    -  @VisibleForTesting
       public static ExoPlaybackException createForUnexpected(RuntimeException cause) {
         return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET);
       }
    @@ -124,7 +122,6 @@ public static ExoPlaybackException createForRemote(String message) {
        * @param cause The cause of the failure.
        * @return The created instance.
        */
    -  @VisibleForTesting
       public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) {
         return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET);
       }
    
    From 0d8146cbcab4ea9b8478d70ee06e0beaefe58dfd Mon Sep 17 00:00:00 2001
    From: olly 
    Date: Thu, 18 Apr 2019 23:44:58 +0100
    Subject: [PATCH 023/219] Further improve DownloadService action names &
     methods
    
    - We had buildAddRequest and sendNewDownload. Converged to
      buildAddDownload and sendAddDownload.
    - Also fixed a few more inconsistencies, and brought the
      action constants into line as well.
    
    PiperOrigin-RevId: 244274041
    ---
     .../exoplayer2/demo/DownloadTracker.java      |  2 +-
     .../exoplayer2/offline/DownloadService.java   | 60 ++++++++++---------
     .../dash/offline/DownloadServiceDashTest.java |  2 +-
     3 files changed, 35 insertions(+), 29 deletions(-)
    
    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 a860d96e43b..f372a47df6f 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
    @@ -263,7 +263,7 @@ private void startDownload() {
         }
     
         private void startDownload(DownloadRequest downloadRequest) {
    -      DownloadService.sendNewDownload(
    +      DownloadService.sendAddDownload(
               context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
         }
     
    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 9de6c748fb1..ea79204c467 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
    @@ -63,7 +63,8 @@ public abstract class DownloadService extends Service {
        *   
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
*/ - public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; + public static final String ACTION_ADD_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: @@ -72,8 +73,8 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_RESUME = - "com.google.android.exoplayer.downloadService.action.RESUME"; + public static final String ACTION_RESUME_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS"; /** * Pauses all downloads. Extras: @@ -82,8 +83,8 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_PAUSE = - "com.google.android.exoplayer.downloadService.action.PAUSE"; + public static final String ACTION_PAUSE_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.PAUSE_DOWNLOADS"; /** * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link @@ -98,7 +99,7 @@ public abstract class DownloadService extends Service { * */ public static final String ACTION_SET_STOP_REASON = - "com.google.android.exoplayer.downloadService.action.SET_MANUAL_STOP_REASON"; + "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; /** * Removes a download. Extras: @@ -108,18 +109,22 @@ public abstract class DownloadService extends Service { *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. * */ - public static final String ACTION_REMOVE = - "com.google.android.exoplayer.downloadService.action.REMOVE"; + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; - /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD} intents. */ + /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE} intents. + * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE_DOWNLOAD} + * intents. */ public static final String KEY_CONTENT_ID = "content_id"; - /** Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD} intents. */ + /** + * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} + * intents. + */ public static final String KEY_STOP_REASON = "manual_stop_reason"; /** @@ -233,12 +238,12 @@ protected DownloadService( * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildAddRequestIntent( + public static Intent buildAddDownloadIntent( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { - return buildAddRequestIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); + return buildAddDownloadIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); } /** @@ -252,13 +257,13 @@ public static Intent buildAddRequestIntent( * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent buildAddRequestIntent( + public static Intent buildAddDownloadIntent( Context context, Class clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground) { - return getIntent(context, clazz, ACTION_ADD, foreground) + return getIntent(context, clazz, ACTION_ADD_DOWNLOAD, foreground) .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) .putExtra(KEY_STOP_REASON, stopReason); } @@ -274,7 +279,8 @@ public static Intent buildAddRequestIntent( */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { - return getIntent(context, clazz, ACTION_REMOVE, foreground).putExtra(KEY_CONTENT_ID, id); + return getIntent(context, clazz, ACTION_REMOVE_DOWNLOAD, foreground) + .putExtra(KEY_CONTENT_ID, id); } /** @@ -287,7 +293,7 @@ public static Intent buildRemoveDownloadIntent( */ public static Intent buildResumeDownloadsIntent( Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_RESUME, foreground); + return getIntent(context, clazz, ACTION_RESUME_DOWNLOADS, foreground); } /** @@ -300,7 +306,7 @@ public static Intent buildResumeDownloadsIntent( */ public static Intent buildPauseDownloadsIntent( Context context, Class clazz, boolean foreground) { - return getIntent(context, clazz, ACTION_PAUSE, foreground); + return getIntent(context, clazz, ACTION_PAUSE_DOWNLOADS, foreground); } /** @@ -333,12 +339,12 @@ public static Intent buildSetStopReasonIntent( * @param downloadRequest The request to be executed. * @param foreground Whether the service is started in the foreground. */ - public static void sendNewDownload( + public static void sendAddDownload( Context context, Class clazz, DownloadRequest downloadRequest, boolean foreground) { - Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, foreground); + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, foreground); startService(context, intent, foreground); } @@ -352,13 +358,13 @@ public static void sendNewDownload( * if the download should be started. * @param foreground Whether the service is started in the foreground. */ - public static void sendNewDownload( + public static void sendAddDownload( Context context, Class clazz, DownloadRequest downloadRequest, int stopReason, boolean foreground) { - Intent intent = buildAddRequestIntent(context, clazz, downloadRequest, stopReason, foreground); + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, stopReason, foreground); startService(context, intent, foreground); } @@ -412,7 +418,7 @@ public static void sendPauseDownloads( * @param stopReason An application defined stop reason. * @param foreground Whether the service is started in the foreground. */ - public static void sendStopReason( + public static void sendSetStopReason( Context context, Class clazz, @Nullable String id, @@ -488,7 +494,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { case ACTION_RESTART: // Do nothing. break; - case ACTION_ADD: + case ACTION_ADD_DOWNLOAD: DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); @@ -497,10 +503,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { downloadManager.addDownload(downloadRequest, stopReason); } break; - case ACTION_RESUME: + case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; - case ACTION_PAUSE: + case ACTION_PAUSE_DOWNLOADS: downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: @@ -512,7 +518,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { downloadManager.setStopReason(contentId, stopReason); } break; - case ACTION_REMOVE: + case ACTION_REMOVE_DOWNLOAD: String contentId = intent.getStringExtra(KEY_CONTENT_ID); if (contentId == null) { Log.e(TAG, "Ignored REMOVE: Missing " + KEY_CONTENT_ID + " extra"); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 57e7b8de5fd..5a9ce2d88e2 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -215,7 +215,7 @@ private void downloadKeys(StreamKey... keys) { dummyMainThread.runOnMainThread( () -> { Intent startIntent = - DownloadService.buildAddRequestIntent( + DownloadService.buildAddDownloadIntent( context, DownloadService.class, action, /* foreground= */ false); dashDownloadService.onStartCommand(startIntent, 0, 0); }); From 7f885351dba24321dcc98b163bba4b3196d73cd6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Apr 2019 00:00:53 +0100 Subject: [PATCH 024/219] Upgrade dependency versions --- extensions/cronet/build.gradle | 2 +- extensions/mediasession/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index ad45f61d989..76972a35301 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:72.3626.96' + api 'org.chromium.net:cronet-embedded:73.3683.76' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'library') diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 186fdb16214..6c6ddf4ce46 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - api 'androidx.media:media:1.0.0' + api 'androidx.media:media:1.0.1' } ext { From b4b82f5b1eb00b668d1abe5004da0e3b8c577316 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Apr 2019 00:04:02 +0100 Subject: [PATCH 025/219] Remove dev-v2 section for 2.10 --- RELEASENOTES.md | 2 -- build.gradle | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 015b348f687..342ca55cc99 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,5 @@ # Release notes # -### dev-v2 (not yet released) ### - ### 2.10.0 ### * Core library: diff --git a/build.gradle b/build.gradle index f8326dd503e..723546726af 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ allprojects { jcenter() } project.ext { - exoplayerPublishEnabled = false + exoplayerPublishEnabled = true } if (it.hasProperty('externalBuildDir')) { if (!new File(externalBuildDir).isAbsolute()) { From 6473d46cbd9e24f9c8b480659be969c67e379937 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Apr 2019 16:05:04 +0100 Subject: [PATCH 026/219] Fix tests --- .../source/dash/offline/DownloadManagerDashTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 9fc9834e1d9..35db882e2a3 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -192,7 +193,6 @@ public void testHandleRemoveAction() throws Throwable { } // Disabled due to flakiness. - @Ignore @Test public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { handleDownloadRequest(fakeStreamKey1); @@ -204,7 +204,6 @@ public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { } // Disabled due to flakiness [Internal: b/122290449]. - @Ignore @Test public void testHandleInterferingRemoveAction() throws Throwable { final ConditionVariable downloadInProgressCondition = new ConditionVariable(); @@ -260,6 +259,7 @@ private void createDownloadManager() { downloadIndex, new DefaultDownloaderFactory( new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); + downloadManager.setRequirements(new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener( From 3d6407a58e6a0762885f73f04add750c5eeaad15 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 13:35:58 +0100 Subject: [PATCH 027/219] Always update loading period in handleSourceInfoRefreshed. This ensures we keep the loading period in sync with the the playing period in PlybackInfo, when the latter changes to something new. PiperOrigin-RevId: 244838123 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a7ee6eb86e3..37774bccb55 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 @@ -1321,7 +1321,6 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } else { // Something changed. Seek to new start position. MediaPeriodHolder periodHolder = queue.getFrontPeriod(); @@ -1341,6 +1340,7 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) playbackInfo.copyWithNewPosition( newPeriodId, seekedToPositionUs, newContentPositionUs, getTotalBufferedDurationUs()); } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } private long getMaxRendererReadPositionUs() { From 615513985677ad3accef8e32370915565b93e3c4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 15:50:30 +0100 Subject: [PATCH 028/219] Fix bug which logs errors twice if stack traces are disabled. Disabling stack trackes currently logs messages twice, once with and once without stack trace. PiperOrigin-RevId: 244853127 --- .../java/com/google/android/exoplayer2/util/Log.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java index 2c3e4f1e7c2..1eb09778475 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -88,8 +88,7 @@ public static void d(String tag, String message) { public static void d(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { d(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel == LOG_LEVEL_ALL) { + } else if (logLevel == LOG_LEVEL_ALL) { android.util.Log.d(tag, message, throwable); } } @@ -105,8 +104,7 @@ public static void i(String tag, String message) { public static void i(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { i(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_INFO) { + } else if (logLevel <= LOG_LEVEL_INFO) { android.util.Log.i(tag, message, throwable); } } @@ -122,8 +120,7 @@ public static void w(String tag, String message) { public static void w(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { w(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_WARNING) { + } else if (logLevel <= LOG_LEVEL_WARNING) { android.util.Log.w(tag, message, throwable); } } @@ -139,8 +136,7 @@ public static void e(String tag, String message) { public static void e(String tag, String message, @Nullable Throwable throwable) { if (!logStackTraces) { e(tag, appendThrowableMessage(message, throwable)); - } - if (logLevel <= LOG_LEVEL_ERROR) { + } else if (logLevel <= LOG_LEVEL_ERROR) { android.util.Log.e(tag, message, throwable); } } From f7f6489f573189f84d8c3f9b0b9ab0797f648d08 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 23 Apr 2019 17:09:05 +0100 Subject: [PATCH 029/219] Add option to add entries in an ActionFile to DownloadIndex as completed PiperOrigin-RevId: 244864742 --- .../exoplayer2/demo/DemoApplication.java | 12 +++-- .../offline/ActionFileUpgradeUtil.java | 14 +++-- .../offline/ActionFileUpgradeUtilTest.java | 51 +++++++++++++++++-- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 2c9cd43d1e3..6985d42b36f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -115,8 +115,10 @@ protected synchronized Cache getDownloadCache() { private synchronized void initDownloadManager() { if (downloadManager == null) { DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); - upgradeActionFile(DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex); - upgradeActionFile(DOWNLOAD_ACTION_FILE, downloadIndex); + upgradeActionFile( + DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); + upgradeActionFile( + DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); DownloaderConstructorHelper downloaderConstructorHelper = new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = @@ -127,13 +129,15 @@ private synchronized void initDownloadManager() { } } - private void upgradeActionFile(String fileName, DefaultDownloadIndex downloadIndex) { + private void upgradeActionFile( + String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { try { ActionFileUpgradeUtil.upgradeAndDelete( new File(getDownloadDirectory(), fileName), /* downloadIdProvider= */ null, downloadIndex, - /* deleteOnFailure= */ true); + /* deleteOnFailure= */ true, + addNewDownloadsAsCompleted); } catch (IOException e) { Log.e(TAG, "Failed to upgrade action file: " + fileName, e); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index b601874f8de..975fc10b93f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -52,6 +52,7 @@ private ActionFileUpgradeUtil() {} * each download will be its custom cache key if one is specified, or else its URL. * @param downloadIndex The index into which the requests will be merged. * @param deleteOnFailure Whether to delete the action file if the merge fails. + * @param addNewDownloadsAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs loading or merging the requests. */ @SuppressWarnings("deprecation") @@ -59,7 +60,8 @@ public static void upgradeAndDelete( File actionFilePath, @Nullable DownloadIdProvider downloadIdProvider, DefaultDownloadIndex downloadIndex, - boolean deleteOnFailure) + boolean deleteOnFailure, + boolean addNewDownloadsAsCompleted) throws IOException { ActionFile actionFile = new ActionFile(actionFilePath); if (actionFile.exists()) { @@ -69,7 +71,7 @@ public static void upgradeAndDelete( if (downloadIdProvider != null) { request = request.copyWithId(downloadIdProvider.getId(request)); } - mergeRequest(request, downloadIndex); + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted); } success = true; } finally { @@ -85,10 +87,14 @@ public static void upgradeAndDelete( * * @param request The request to be merged. * @param downloadIndex The index into which the request will be merged. + * @param addNewDownloadAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs merging the request. */ /* package */ static void mergeRequest( - DownloadRequest request, DefaultDownloadIndex downloadIndex) throws IOException { + DownloadRequest request, + DefaultDownloadIndex downloadIndex, + boolean addNewDownloadAsCompleted) + throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { download = DownloadManager.mergeRequest(download, request, download.stopReason); @@ -97,7 +103,7 @@ public static void upgradeAndDelete( download = new Download( request, - STATE_QUEUED, + addNewDownloadAsCompleted ? Download.STATE_COMPLETED : STATE_QUEUED, /* startTimeMs= */ nowMs, /* updateTimeMs= */ nowMs, /* contentLength= */ C.LENGTH_UNSET, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java index 96b8ff21bc8..dba7b74e9f5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java @@ -88,7 +88,11 @@ public void upgradeAndDelete_createsDownloads() throws IOException { new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.upgradeAndDelete( - tempFile, /* downloadIdProvider= */ null, downloadIndex, /* deleteOnFailure= */ true); + tempFile, + /* downloadIdProvider= */ null, + downloadIndex, + /* deleteOnFailure= */ true, + /* addNewDownloadsAsCompleted= */ false); assertDownloadIndexContainsRequest(expectedRequest1, Download.STATE_QUEUED); assertDownloadIndexContainsRequest(expectedRequest2, Download.STATE_QUEUED); @@ -108,7 +112,8 @@ public void mergeRequest_nonExistingDownload_createsNewDownload() throws IOExcep /* customCacheKey= */ "key123", data); - ActionFileUpgradeUtil.mergeRequest(request, downloadIndex); + ActionFileUpgradeUtil.mergeRequest( + request, downloadIndex, /* addNewDownloadAsCompleted= */ false); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -135,8 +140,10 @@ public void mergeRequest_existingDownload_createsMergedDownload() throws IOExcep asList(streamKey2), /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); - ActionFileUpgradeUtil.mergeRequest(request1, downloadIndex); - ActionFileUpgradeUtil.mergeRequest(request2, downloadIndex); + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + ActionFileUpgradeUtil.mergeRequest( + request2, downloadIndex, /* addNewDownloadAsCompleted= */ false); Download download = downloadIndex.getDownload(request2.id); assertThat(download).isNotNull(); @@ -148,6 +155,42 @@ public void mergeRequest_existingDownload_createsMergedDownload() throws IOExcep assertThat(download.state).isEqualTo(Download.STATE_QUEUED); } + @Test + public void mergeRequest_addNewDownloadAsCompleted() throws IOException { + StreamKey streamKey1 = + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5); + StreamKey streamKey2 = + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2); + DownloadRequest request1 = + new DownloadRequest( + "id1", + TYPE_PROGRESSIVE, + Uri.parse("https://www.test.com/download1"), + asList(streamKey1), + /* customCacheKey= */ "key123", + new byte[] {1, 2, 3, 4}); + DownloadRequest request2 = + new DownloadRequest( + "id2", + TYPE_PROGRESSIVE, + Uri.parse("https://www.test.com/download2"), + asList(streamKey2), + /* customCacheKey= */ "key123", + new byte[] {5, 4, 3, 2, 1}); + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + + // Merging existing download, keeps it queued. + ActionFileUpgradeUtil.mergeRequest( + request1, downloadIndex, /* addNewDownloadAsCompleted= */ true); + assertThat(downloadIndex.getDownload(request1.id).state).isEqualTo(Download.STATE_QUEUED); + + // New download is merged as completed. + ActionFileUpgradeUtil.mergeRequest( + request2, downloadIndex, /* addNewDownloadAsCompleted= */ true); + assertThat(downloadIndex.getDownload(request2.id).state).isEqualTo(Download.STATE_COMPLETED); + } + private void assertDownloadIndexContainsRequest(DownloadRequest request, int state) throws IOException { Download download = downloadIndex.getDownload(request.id); From 4da14e46fa7ded200c11771a19946949fb9c34da Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 24 Apr 2019 11:08:00 +0100 Subject: [PATCH 030/219] Add DownloadService SET_REQUIREMENTS action PiperOrigin-RevId: 245014381 --- .../exoplayer2/offline/DownloadManager.java | 4 +- .../exoplayer2/offline/DownloadService.java | 103 ++++++++++++++---- .../exoplayer2/scheduler/Requirements.java | 34 +++++- 3 files changed, 113 insertions(+), 28 deletions(-) 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 d4df5cd18b5..74332c08f3e 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 @@ -268,9 +268,9 @@ public Requirements getRequirements() { } /** - * Sets the requirements needed to be met to start downloads. + * Sets the requirements that need to be met for downloads to progress. * - * @param requirements Need to be met to start downloads. + * @param requirements A {@link Requirements}. */ public void setRequirements(Requirements requirements) { if (requirements.equals(requirementsWatcher.getRequirements())) { 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 ea79204c467..ee00cf3d5fc 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 @@ -66,6 +66,17 @@ public abstract class DownloadService extends Service { public static final String ACTION_ADD_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; + /** + * Removes a download. Extras: + * + *
      + *
    • {@link #KEY_CONTENT_ID} - The content id of a download to remove. + *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
    + */ + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -91,10 +102,10 @@ public abstract class DownloadService extends Service { * Download#STOP_REASON_NONE}. Extras: * *
      - *
    • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the manual - * stop reason. If omitted, all downloads will be updated. + *
    • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop + * reason. If omitted, all downloads will be updated. *
    • {@link #KEY_STOP_REASON} - An application provided reason for stopping the download or - * downloads, or {@link Download#STOP_REASON_NONE} to clear the manual stop reason. + * downloads, or {@link Download#STOP_REASON_NONE} to clear the stop reason. *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ @@ -102,15 +113,15 @@ public abstract class DownloadService extends Service { "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; /** - * Removes a download. Extras: + * Sets the requirements that need to be met for downloads to progress. Extras: * *
      - *
    • {@link #KEY_CONTENT_ID} - The content id of a download to remove. + *
    • {@link #KEY_REQUIREMENTS} - A {@link Requirements}. *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. *
    */ - public static final String ACTION_REMOVE_DOWNLOAD = - "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + public static final String ACTION_SET_REQUIREMENTS = + "com.google.android.exoplayer.downloadService.action.SET_REQUIREMENTS"; /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_DOWNLOAD_REQUEST = "download_request"; @@ -125,7 +136,10 @@ public abstract class DownloadService extends Service { * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} * intents. */ - public static final String KEY_STOP_REASON = "manual_stop_reason"; + public static final String KEY_STOP_REASON = "stop_reason"; + + /** Key for the requirements in {@link #ACTION_SET_REQUIREMENTS} intents. */ + public static final String KEY_REQUIREMENTS = "requirements"; /** * Key for a boolean extra that can be set on any intent to indicate whether the service was @@ -236,7 +250,7 @@ protected DownloadService( * @param clazz The concrete download service being targeted by the intent. * @param downloadRequest The request to be executed. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildAddDownloadIntent( Context context, @@ -255,7 +269,7 @@ public static Intent buildAddDownloadIntent( * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} * if the download should be started. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildAddDownloadIntent( Context context, @@ -275,7 +289,7 @@ public static Intent buildAddDownloadIntent( * @param clazz The concrete download service being targeted by the intent. * @param id The content id. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildRemoveDownloadIntent( Context context, Class clazz, String id, boolean foreground) { @@ -289,7 +303,7 @@ public static Intent buildRemoveDownloadIntent( * @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 Created Intent. + * @return The created intent. */ public static Intent buildResumeDownloadsIntent( Context context, Class clazz, boolean foreground) { @@ -302,7 +316,7 @@ public static Intent buildResumeDownloadsIntent( * @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 Created Intent. + * @return The created intent. */ public static Intent buildPauseDownloadsIntent( Context context, Class clazz, boolean foreground) { @@ -318,7 +332,7 @@ public static Intent buildPauseDownloadsIntent( * @param id The content id, or {@code null} to set the stop reason for all downloads. * @param stopReason An application defined stop reason. * @param foreground Whether this intent will be used to start the service in the foreground. - * @return Created Intent. + * @return The created intent. */ public static Intent buildSetStopReasonIntent( Context context, @@ -331,6 +345,25 @@ public static Intent buildSetStopReasonIntent( .putExtra(KEY_STOP_REASON, stopReason); } + /** + * Builds an {@link Intent} for setting the requirements that need to be met for downloads to + * progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param requirements A {@link Requirements}. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetRequirementsIntent( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_REQUIREMENTS, foreground) + .putExtra(KEY_REQUIREMENTS, requirements); + } + /** * Starts the service if not started already and adds a new download. * @@ -428,6 +461,24 @@ public static void sendSetStopReason( startService(context, intent, foreground); } + /** + * Starts the service if not started already and sets the requirements that need to be met for + * downloads to progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param requirements A {@link Requirements}. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetRequirements( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + Intent intent = buildSetRequirementsIntent(context, clazz, requirements, foreground); + startService(context, intent, foreground); + } + /** * Starts a download service to resume any ongoing downloads. * @@ -479,10 +530,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; String intentAction = null; + String contentId = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); + contentId = intent.getStringExtra(KEY_CONTENT_ID); } // intentAction is null if the service is restarted or no action is specified. if (intentAction == null) { @@ -497,12 +550,19 @@ public int onStartCommand(Intent intent, int flags, int startId) { case ACTION_ADD_DOWNLOAD: DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { - Log.e(TAG, "Ignored ADD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); + Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); downloadManager.addDownload(downloadRequest, stopReason); } break; + case ACTION_REMOVE_DOWNLOAD: + if (contentId == null) { + Log.e(TAG, "Ignored REMOVE_DOWNLOAD: Missing " + KEY_CONTENT_ID + " extra"); + } else { + downloadManager.removeDownload(contentId); + } + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; @@ -511,19 +571,18 @@ public int onStartCommand(Intent intent, int flags, int startId) { break; case ACTION_SET_STOP_REASON: if (!intent.hasExtra(KEY_STOP_REASON)) { - Log.e(TAG, "Ignored SET_MANUAL_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); + Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { - String contentId = intent.getStringExtra(KEY_CONTENT_ID); int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); downloadManager.setStopReason(contentId, stopReason); } break; - case ACTION_REMOVE_DOWNLOAD: - String contentId = intent.getStringExtra(KEY_CONTENT_ID); - if (contentId == null) { - Log.e(TAG, "Ignored REMOVE: Missing " + KEY_CONTENT_ID + " extra"); + case ACTION_SET_REQUIREMENTS: + Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + if (requirements == null) { + Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { - downloadManager.removeDownload(contentId); + downloadManager.setRequirements(requirements); } break; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 28aa37ee2ab..babc4e49fba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -23,6 +23,8 @@ import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.os.BatteryManager; +import android.os.Parcel; +import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Log; @@ -31,10 +33,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Defines a set of device state requirements. - */ -public final class Requirements { +/** Defines a set of device state requirements. */ +public final class Requirements implements Parcelable { /** * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED}, @@ -205,4 +205,30 @@ public boolean equals(Object o) { public int hashCode() { return requirements; } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requirements); + } + + public static final Parcelable.Creator CREATOR = + new Creator() { + + @Override + public Requirements createFromParcel(Parcel in) { + return new Requirements(in.readInt()); + } + + @Override + public Requirements[] newArray(int size) { + return new Requirements[size]; + } + }; } From 7626ff72de8e6d9feab54980e6dee13dcab8361f Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:32:07 +0100 Subject: [PATCH 031/219] Update gradle plugin. This also removes the build warning about the experimental flag. PiperOrigin-RevId: 245218251 --- gradle.properties | 1 - gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 364a5d03c52..4b9bfa8fa2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ ## Project-wide Gradle settings. android.useAndroidX=true android.enableJetifier=true -android.useDeprecatedNdk=true android.enableUnitTestBinaryResources=true buildDir=buildout diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7061ab9fe70..6d00e1ce970 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Feb 08 20:49:20 GMT 2019 +#Thu Apr 25 13:15:25 BST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip From 249f6a77ee31c05e486df5d37e2adbab889cfdaa Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:38:54 +0100 Subject: [PATCH 032/219] Update gradle plugin (part 2). PiperOrigin-RevId: 245218900 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 723546726af..4761a1fbe0c 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } From c97ee9429ba8c7284268f0b9abd1b0584c23ee1c Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 10:25:21 +0100 Subject: [PATCH 033/219] Allow content id to be set in DownloadHelper.getDownloadRequest PiperOrigin-RevId: 245388082 --- .../exoplayer2/offline/DownloadHelper.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 c9b0451f415..8a15c82c893 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 @@ -578,16 +578,27 @@ public void addTrackSelectionForSingleRenderer( /** * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until - * after preparation completes. + * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id. * * @param data Application provided data to store in {@link DownloadRequest#data}. * @return The built {@link DownloadRequest}. */ public DownloadRequest getDownloadRequest(@Nullable byte[] data) { - String downloadId = uri.toString(); + return getDownloadRequest(uri.toString(), data); + } + + /** + * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until + * after preparation completes. + * + * @param id The unique content id. + * @param data Application provided data to store in {@link DownloadRequest#data}. + * @return The built {@link DownloadRequest}. + */ + public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { if (mediaSource == null) { return new DownloadRequest( - downloadId, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); + id, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); } assertPreparedWithMedia(); List streamKeys = new ArrayList<>(); @@ -601,7 +612,7 @@ public DownloadRequest getDownloadRequest(@Nullable byte[] data) { } streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); } - return new DownloadRequest(downloadId, downloadType, uri, streamKeys, cacheKey, data); + return new DownloadRequest(id, downloadType, uri, streamKeys, cacheKey, data); } // Initialization of array of Lists. From fc35d5fca6b0f8c505376583a040a602a7094dfa Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 12:05:09 +0100 Subject: [PATCH 034/219] Add simpler DownloadManager constructor PiperOrigin-RevId: 245397736 --- .../exoplayer2/offline/DownloadManager.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 74332c08f3e..bfcb5174cc5 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 @@ -34,8 +34,14 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheEvictor; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -183,6 +189,24 @@ default void onRequirementsStateChanged( private volatile int maxParallelDownloads; private volatile int minRetryCount; + /** + * Constructs a {@link DownloadManager}. + * + * @param context Any context. + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param cache A cache to be used to store downloaded data. The cache should be configured with + * an {@link CacheEvictor} that will not evict downloaded content, for example {@link + * NoOpCacheEvictor}. + * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data. + */ + public DownloadManager( + Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) { + this( + context, + new DefaultDownloadIndex(databaseProvider), + new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); + } + /** * Constructs a {@link DownloadManager}. * From 9d03ae41095495df6e0f4f4ff6aee847610c8582 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 12:46:48 +0100 Subject: [PATCH 035/219] Add missing getters and clarify STATE_QUEUED documentation PiperOrigin-RevId: 245401274 --- .../android/exoplayer2/offline/Download.java | 12 +++- .../exoplayer2/offline/DownloadManager.java | 64 +++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 9f6b473208b..00d81b392c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -43,7 +43,17 @@ public final class Download { }) public @interface State {} // Important: These constants are persisted into DownloadIndex. Do not change them. - /** The download is waiting to be started. */ + /** + * The download is waiting to be started. A download may be queued because the {@link + * DownloadManager} + * + *
      + *
    • Is {@link DownloadManager#getDownloadsPaused() paused} + *
    • Has {@link DownloadManager#getRequirements() Requirements} that are not met + *
    • Has already started {@link DownloadManager#getMaxParallelDownloads() + * maxParallelDownloads} + *
    + */ public static final int STATE_QUEUED = 0; /** The download is stopped for a specified {@link #stopReason}. */ public static final int STATE_STOPPED = 1; 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 bfcb5174cc5..0ca13e23854 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 @@ -130,7 +130,7 @@ default void onRequirementsStateChanged( // Messages posted to the background handler. private static final int MSG_INITIALIZE = 0; - private static final int MSG_SET_DOWNLOADS_RESUMED = 1; + private static final int MSG_SET_DOWNLOADS_PAUSED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; private static final int MSG_SET_STOP_REASON = 3; private static final int MSG_ADD_DOWNLOAD = 4; @@ -178,11 +178,12 @@ default void onRequirementsStateChanged( private int activeDownloadCount; private boolean initialized; private boolean released; + private boolean downloadsPaused; private RequirementsWatcher requirementsWatcher; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; - private boolean downloadsResumed; + private boolean downloadsPausedInternal; private int parallelDownloads; // TODO: Fix these to properly support changes at runtime. @@ -221,6 +222,8 @@ public DownloadManager( this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; + downloadsPaused = true; + downloadsPausedInternal = true; downloadInternals = new ArrayList<>(); downloads = new ArrayList<>(); @@ -306,6 +309,11 @@ public void setRequirements(Requirements requirements) { onRequirementsStateChanged(requirementsWatcher, notMetRequirements); } + /** Returns the maximum number of parallel downloads. */ + public int getMaxParallelDownloads() { + return maxParallelDownloads; + } + /** * Sets the maximum number of parallel downloads. * @@ -316,6 +324,14 @@ public void setMaxParallelDownloads(int maxParallelDownloads) { this.maxParallelDownloads = maxParallelDownloads; } + /** + * Returns the minimum number of times that a download will be retried. A download will fail if + * the specified number of retries is exceeded without any progress being made. + */ + public int getMinRetryCount() { + return minRetryCount; + } + /** * Sets the minimum number of times that a download will be retried. A download will fail if the * specified number of retries is exceeded without any progress being made. @@ -341,19 +357,41 @@ public List getCurrentDownloads() { return Collections.unmodifiableList(new ArrayList<>(downloads)); } - /** Resumes all downloads except those that have a non-zero {@link Download#stopReason}. */ + /** Returns whether downloads are currently paused. */ + public boolean getDownloadsPaused() { + return downloadsPaused; + } + + /** + * Resumes downloads. + * + *

    If the {@link #setRequirements(Requirements) Requirements} are met up to {@link + * #getMaxParallelDownloads() maxParallelDownloads} will be started, excluding those with non-zero + * {@link Download#stopReason stopReasons}. + */ public void resumeDownloads() { + if (!downloadsPaused) { + return; + } + downloadsPaused = false; pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 1, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 0, /* unused */ 0) .sendToTarget(); } - /** Pauses all downloads. */ + /** + * Pauses downloads. Downloads that would otherwise be making progress transition to {@link + * Download#STATE_QUEUED}. + */ public void pauseDownloads() { + if (downloadsPaused) { + return; + } + downloadsPaused = true; pendingMessages++; internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_RESUMED, /* downloadsResumed */ 0, /* unused */ 0) + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 1, /* unused */ 0) .sendToTarget(); } @@ -536,9 +574,9 @@ private boolean handleInternalMessage(Message message) { int notMetRequirements = message.arg1; initializeInternal(notMetRequirements); break; - case MSG_SET_DOWNLOADS_RESUMED: - boolean downloadsResumed = message.arg1 != 0; - setDownloadsResumed(downloadsResumed); + case MSG_SET_DOWNLOADS_PAUSED: + boolean downloadsPaused = message.arg1 != 0; + setDownloadsPausedInternal(downloadsPaused); break; case MSG_SET_NOT_MET_REQUIREMENTS: notMetRequirements = message.arg1; @@ -604,11 +642,11 @@ private void initializeInternal(int notMetRequirements) { } } - private void setDownloadsResumed(boolean downloadsResumed) { - if (this.downloadsResumed == downloadsResumed) { + private void setDownloadsPausedInternal(boolean downloadsPaused) { + if (this.downloadsPausedInternal == downloadsPaused) { return; } - this.downloadsResumed = downloadsResumed; + this.downloadsPausedInternal = downloadsPaused; for (int i = 0; i < downloadInternals.size(); i++) { downloadInternals.get(i).updateStopState(); } @@ -820,7 +858,7 @@ private void addDownloadForState(Download download) { } private boolean canStartDownloads() { - return downloadsResumed && notMetRequirements == 0; + return !downloadsPausedInternal && notMetRequirements == 0; } /* package */ static Download mergeRequest( From d187d9ec8fa252fdd25333a90116b3e11a9a3afb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 13:30:48 +0100 Subject: [PATCH 036/219] Post maxParallelDownload and minRetryCount changes PiperOrigin-RevId: 245405316 --- .../exoplayer2/offline/DownloadManager.java | 68 +++++++++++++++---- 1 file changed, 53 insertions(+), 15 deletions(-) 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 0ca13e23854..91a767cfabd 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,11 +133,13 @@ default void onRequirementsStateChanged( private static final int MSG_SET_DOWNLOADS_PAUSED = 1; private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; private static final int MSG_SET_STOP_REASON = 3; - private static final int MSG_ADD_DOWNLOAD = 4; - private static final int MSG_REMOVE_DOWNLOAD = 5; - private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; - private static final int MSG_CONTENT_LENGTH_CHANGED = 7; - private static final int MSG_RELEASE = 8; + private static final int MSG_SET_MAX_PARALLEL_DOWNLOADS = 4; + 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_DOWNLOAD_THREAD_STOPPED = 8; + private static final int MSG_CONTENT_LENGTH_CHANGED = 9; + private static final int MSG_RELEASE = 10; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -179,17 +181,17 @@ default void onRequirementsStateChanged( private boolean initialized; private boolean released; private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; private RequirementsWatcher requirementsWatcher; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; private boolean downloadsPausedInternal; + private int maxParallelDownloadsInternal; + private int minRetryCountInternal; private int parallelDownloads; - // TODO: Fix these to properly support changes at runtime. - private volatile int maxParallelDownloads; - private volatile int minRetryCount; - /** * Constructs a {@link DownloadManager}. * @@ -221,7 +223,9 @@ public DownloadManager( this.downloadIndex = downloadIndex; this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; + maxParallelDownloadsInternal = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; + minRetryCountInternal = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; downloadsPausedInternal = true; @@ -319,9 +323,15 @@ public int getMaxParallelDownloads() { * * @param maxParallelDownloads The maximum number of parallel downloads. */ - // TODO: Fix to properly support changes at runtime. public void setMaxParallelDownloads(int maxParallelDownloads) { + if (this.maxParallelDownloads == maxParallelDownloads) { + return; + } this.maxParallelDownloads = maxParallelDownloads; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MAX_PARALLEL_DOWNLOADS, maxParallelDownloads, /* unused */ 0) + .sendToTarget(); } /** @@ -338,9 +348,15 @@ public int getMinRetryCount() { * * @param minRetryCount The minimum number of times that a download will be retried. */ - // TODO: Fix to properly support changes at runtime. public void setMinRetryCount(int minRetryCount) { + if (this.minRetryCount == minRetryCount) { + return; + } this.minRetryCount = minRetryCount; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MIN_RETRY_COUNT, minRetryCount, /* unused */ 0) + .sendToTarget(); } /** Returns the used {@link DownloadIndex}. */ @@ -587,6 +603,14 @@ private boolean handleInternalMessage(Message message) { int stopReason = message.arg1; setStopReasonInternal(id, stopReason); break; + case MSG_SET_MAX_PARALLEL_DOWNLOADS: + int maxParallelDownloads = message.arg1; + setMaxParallelDownloadsInternal(maxParallelDownloads); + break; + case MSG_SET_MIN_RETRY_COUNT: + int minRetryCount = message.arg1; + setMinRetryCountInternal(minRetryCount); + break; case MSG_ADD_DOWNLOAD: DownloadRequest request = (DownloadRequest) message.obj; stopReason = message.arg1; @@ -688,6 +712,15 @@ private void setStopReasonInternal(@Nullable String id, int stopReason) { } } + private void setMaxParallelDownloadsInternal(int maxParallelDownloads) { + maxParallelDownloadsInternal = maxParallelDownloads; + // TODO: Start or stop downloads if necessary. + } + + private void setMinRetryCountInternal(int minRetryCount) { + minRetryCountInternal = minRetryCount; + } + private void addDownloadInternal(DownloadRequest request, int stopReason) { DownloadInternal downloadInternal = getDownload(request.id); if (downloadInternal != null) { @@ -736,14 +769,14 @@ private void onDownloadThreadStoppedInternal(DownloadThread downloadThread) { boolean tryToStartDownloads = false; if (!downloadThread.isRemove) { // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloads; + tryToStartDownloads = parallelDownloads == maxParallelDownloadsInternal; parallelDownloads--; } getDownload(downloadId) .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); if (tryToStartDownloads) { for (int i = 0; - parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); + parallelDownloads < maxParallelDownloadsInternal && i < downloadInternals.size(); i++) { downloadInternals.get(i).start(); } @@ -804,7 +837,7 @@ private int startDownloadThread(DownloadInternal downloadInternal) { } boolean isRemove = downloadInternal.isInRemoveState(); if (!isRemove) { - if (parallelDownloads == maxParallelDownloads) { + if (parallelDownloads == maxParallelDownloadsInternal) { return START_THREAD_TOO_MANY_DOWNLOADS; } parallelDownloads++; @@ -813,7 +846,12 @@ private int startDownloadThread(DownloadInternal downloadInternal) { DownloadProgress downloadProgress = downloadInternal.download.progress; DownloadThread downloadThread = new DownloadThread( - request, downloader, downloadProgress, isRemove, minRetryCount, internalHandler); + request, + downloader, + downloadProgress, + isRemove, + minRetryCountInternal, + internalHandler); downloadThreads.put(downloadId, downloadThread); downloadThread.start(); logd("Download is started", downloadInternal); From 56520b7c731ca41088f18e6a7c3ded28f7346a00 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 13:53:58 +0100 Subject: [PATCH 037/219] Move DownloadManager internal logic into isolated inner class There are no logic changes here. It's just moving code around and removing the "internal" part of names where no longer required. PiperOrigin-RevId: 245407238 --- .../exoplayer2/offline/DownloadManager.java | 763 +++++++++--------- 1 file changed, 386 insertions(+), 377 deletions(-) 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 91a767cfabd..aa0cd12231d 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 @@ -160,38 +160,21 @@ default void onRequirementsStateChanged( private final Context context; private final WritableDownloadIndex downloadIndex; - private final DownloaderFactory downloaderFactory; private final Handler mainHandler; - private final HandlerThread internalThread; - private final Handler internalHandler; + private final InternalHandler internalHandler; private final RequirementsWatcher.Listener requirementsListener; - private final Object releaseLock; - // Collections that are accessed on the main thread. private final CopyOnWriteArraySet listeners; private final ArrayList downloads; - // Collections that are accessed on the internal thread. - private final ArrayList downloadInternals; - private final HashMap downloadThreads; - - // Mutable fields that are accessed on the main thread. private int pendingMessages; private int activeDownloadCount; private boolean initialized; - private boolean released; private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; private RequirementsWatcher requirementsWatcher; - // Mutable fields that are accessed on the internal thread. - @Requirements.RequirementFlags private int notMetRequirements; - private boolean downloadsPausedInternal; - private int maxParallelDownloadsInternal; - private int minRetryCountInternal; - private int parallelDownloads; - /** * Constructs a {@link DownloadManager}. * @@ -221,31 +204,29 @@ public DownloadManager( Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { this.context = context.getApplicationContext(); this.downloadIndex = downloadIndex; - this.downloaderFactory = downloaderFactory; maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; - maxParallelDownloadsInternal = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; - minRetryCountInternal = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; - downloadsPausedInternal = true; - - downloadInternals = new ArrayList<>(); downloads = new ArrayList<>(); - downloadThreads = new HashMap<>(); listeners = new CopyOnWriteArraySet<>(); - releaseLock = new Object(); - requirementsListener = this::onRequirementsStateChanged; - - mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); - internalThread = new HandlerThread("DownloadManager file i/o"); - internalThread.start(); - internalHandler = new Handler(internalThread.getLooper(), this::handleInternalMessage); - requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); int notMetRequirements = requirementsWatcher.start(); + mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); + HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); + internalThread.start(); + internalHandler = + new InternalHandler( + internalThread, + downloadIndex, + downloaderFactory, + mainHandler, + maxParallelDownloads, + minRetryCount, + downloadsPaused); + pendingMessages = 1; internalHandler .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0) @@ -464,15 +445,15 @@ public void removeDownload(String id) { * download index. The manager must not be accessed after this method has been called. */ public void release() { - synchronized (releaseLock) { - if (released) { + synchronized (internalHandler) { + if (internalHandler.released) { return; } internalHandler.sendEmptyMessage(MSG_RELEASE); boolean wasInterrupted = false; - while (!released) { + while (!internalHandler.released) { try { - releaseLock.wait(); + internalHandler.wait(); } catch (InterruptedException e) { wasInterrupted = true; } @@ -581,383 +562,411 @@ private int getDownloadIndex(String id) { return C.INDEX_UNSET; } - // Internal thread message handling. - - private boolean handleInternalMessage(Message message) { - boolean processedExternalMessage = true; - switch (message.what) { - case MSG_INITIALIZE: - int notMetRequirements = message.arg1; - initializeInternal(notMetRequirements); - break; - case MSG_SET_DOWNLOADS_PAUSED: - boolean downloadsPaused = message.arg1 != 0; - setDownloadsPausedInternal(downloadsPaused); - break; - case MSG_SET_NOT_MET_REQUIREMENTS: - notMetRequirements = message.arg1; - setNotMetRequirementsInternal(notMetRequirements); - break; - case MSG_SET_STOP_REASON: - String id = (String) message.obj; - int stopReason = message.arg1; - setStopReasonInternal(id, stopReason); - break; - case MSG_SET_MAX_PARALLEL_DOWNLOADS: - int maxParallelDownloads = message.arg1; - setMaxParallelDownloadsInternal(maxParallelDownloads); - break; - case MSG_SET_MIN_RETRY_COUNT: - int minRetryCount = message.arg1; - setMinRetryCountInternal(minRetryCount); - break; - case MSG_ADD_DOWNLOAD: - DownloadRequest request = (DownloadRequest) message.obj; - stopReason = message.arg1; - addDownloadInternal(request, stopReason); - break; - case MSG_REMOVE_DOWNLOAD: - id = (String) message.obj; - removeDownloadInternal(id); - break; - case MSG_DOWNLOAD_THREAD_STOPPED: - DownloadThread downloadThread = (DownloadThread) message.obj; - onDownloadThreadStoppedInternal(downloadThread); - processedExternalMessage = false; // This message is posted internally. - break; - case MSG_CONTENT_LENGTH_CHANGED: - downloadThread = (DownloadThread) message.obj; - onDownloadThreadContentLengthChangedInternal(downloadThread); - processedExternalMessage = false; // This message is posted internally. - break; - case MSG_RELEASE: - releaseInternal(); - return true; // Don't post back to mainHandler on release. - default: - throw new IllegalStateException(); + /* package */ static Download mergeRequest( + Download download, DownloadRequest request, int stopReason) { + @Download.State int state = download.state; + if (state == STATE_REMOVING || state == STATE_RESTARTING) { + state = STATE_RESTARTING; + } else if (stopReason != STOP_REASON_NONE) { + state = STATE_STOPPED; + } else { + state = STATE_QUEUED; } - mainHandler - .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) - .sendToTarget(); - return true; + long nowMs = System.currentTimeMillis(); + long startTimeMs = download.isTerminalState() ? nowMs : download.startTimeMs; + return new Download( + download.request.copyWithMergedRequest(request), + state, + startTimeMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE); } - private void initializeInternal(int notMetRequirements) { - this.notMetRequirements = notMetRequirements; - ArrayList loadedStates = new ArrayList<>(); - try (DownloadCursor cursor = - downloadIndex.getDownloads( - STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { - while (cursor.moveToNext()) { - loadedStates.add(cursor.getDownload()); - } - logd("Downloads are loaded."); - } catch (Throwable e) { - Log.e(TAG, "Download state loading failed.", e); - loadedStates.clear(); - } - for (Download download : loadedStates) { - addDownloadForState(download); - } - logd("Downloads are created."); - mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).start(); + private static Download copyWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + download.stopReason, + FAILURE_REASON_NONE, + download.progress); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); } } - private void setDownloadsPausedInternal(boolean downloadsPaused) { - if (this.downloadsPausedInternal == downloadsPaused) { - return; + private static void logd(String message, DownloadInternal downloadInternal) { + logd(message, downloadInternal.download.request); + } + + private static void logd(String message, DownloadRequest request) { + if (DEBUG) { + logd(message + ": " + request); } - this.downloadsPausedInternal = downloadsPaused; - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); + } + + private static void logdFlags(String message, int flags) { + if (DEBUG) { + logd(message + ": " + Integer.toBinaryString(flags)); } } - private void setNotMetRequirementsInternal( - @Requirements.RequirementFlags int notMetRequirements) { - if (this.notMetRequirements == notMetRequirements) { - return; + private static final class InternalHandler extends Handler { + + public boolean released; + + private final HandlerThread thread; + private final WritableDownloadIndex downloadIndex; + private final DownloaderFactory downloaderFactory; + private final Handler mainHandler; + private final ArrayList downloadInternals; + private final HashMap downloadThreads; + + // Mutable fields that are accessed on the internal thread. + @Requirements.RequirementFlags private int notMetRequirements; + private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; + private int parallelDownloads; + + public InternalHandler( + HandlerThread thread, + WritableDownloadIndex downloadIndex, + DownloaderFactory downloaderFactory, + Handler mainHandler, + int maxParallelDownloads, + int minRetryCount, + boolean downloadsPaused) { + super(thread.getLooper()); + this.thread = thread; + this.downloadIndex = downloadIndex; + this.downloaderFactory = downloaderFactory; + this.mainHandler = mainHandler; + this.maxParallelDownloads = maxParallelDownloads; + this.minRetryCount = minRetryCount; + this.downloadsPaused = downloadsPaused; + downloadInternals = new ArrayList<>(); + downloadThreads = new HashMap<>(); } - this.notMetRequirements = notMetRequirements; - logdFlags("Not met requirements are changed", notMetRequirements); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); + + @Override + public void handleMessage(Message message) { + boolean processedExternalMessage = true; + switch (message.what) { + case MSG_INITIALIZE: + int notMetRequirements = message.arg1; + initialize(notMetRequirements); + break; + case MSG_SET_DOWNLOADS_PAUSED: + boolean downloadsPaused = message.arg1 != 0; + setDownloadsPaused(downloadsPaused); + break; + case MSG_SET_NOT_MET_REQUIREMENTS: + notMetRequirements = message.arg1; + setNotMetRequirements(notMetRequirements); + break; + case MSG_SET_STOP_REASON: + String id = (String) message.obj; + int stopReason = message.arg1; + setStopReason(id, stopReason); + break; + case MSG_SET_MAX_PARALLEL_DOWNLOADS: + int maxParallelDownloads = message.arg1; + setMaxParallelDownloads(maxParallelDownloads); + break; + case MSG_SET_MIN_RETRY_COUNT: + int minRetryCount = message.arg1; + setMinRetryCount(minRetryCount); + break; + case MSG_ADD_DOWNLOAD: + DownloadRequest request = (DownloadRequest) message.obj; + stopReason = message.arg1; + addDownload(request, stopReason); + break; + case MSG_REMOVE_DOWNLOAD: + id = (String) message.obj; + removeDownload(id); + break; + case MSG_DOWNLOAD_THREAD_STOPPED: + DownloadThread downloadThread = (DownloadThread) message.obj; + onDownloadThreadStopped(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; + case MSG_CONTENT_LENGTH_CHANGED: + downloadThread = (DownloadThread) message.obj; + onDownloadThreadContentLengthChanged(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; + case MSG_RELEASE: + release(); + return; // Don't post back to mainHandler on release. + default: + throw new IllegalStateException(); + } + mainHandler + .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) + .sendToTarget(); + } + + private void initialize(int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + ArrayList loadedStates = new ArrayList<>(); + try (DownloadCursor cursor = + downloadIndex.getDownloads( + STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { + while (cursor.moveToNext()) { + loadedStates.add(cursor.getDownload()); + } + logd("Downloads are loaded."); + } catch (Throwable e) { + Log.e(TAG, "Download state loading failed.", e); + loadedStates.clear(); + } + for (Download download : loadedStates) { + addDownloadForState(download); + } + logd("Downloads are created."); + mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).start(); + } } - } - private void setStopReasonInternal(@Nullable String id, int stopReason) { - if (id != null) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - logd("download stop reason is set to : " + stopReason, downloadInternal); - downloadInternal.setStopReason(stopReason); + private void setDownloadsPaused(boolean downloadsPaused) { + this.downloadsPaused = downloadsPaused; + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).updateStopState(); + } + } + + private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + // TODO: Move this deduplication check to the main thread. + if (this.notMetRequirements == notMetRequirements) { return; } - } else { + this.notMetRequirements = notMetRequirements; + logdFlags("Not met requirements are changed", notMetRequirements); for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setStopReason(stopReason); + downloadInternals.get(i).updateStopState(); } } - try { + + private void setStopReason(@Nullable String id, int stopReason) { if (id != null) { - downloadIndex.setStopReason(id, stopReason); + DownloadInternal downloadInternal = getDownload(id); + if (downloadInternal != null) { + logd("download stop reason is set to : " + stopReason, downloadInternal); + downloadInternal.setStopReason(stopReason); + return; + } } else { - downloadIndex.setStopReason(stopReason); + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).setStopReason(stopReason); + } + } + try { + if (id != null) { + downloadIndex.setStopReason(id, stopReason); + } else { + downloadIndex.setStopReason(stopReason); + } + } catch (IOException e) { + Log.e(TAG, "setStopReason failed", e); } - } catch (IOException e) { - Log.e(TAG, "setStopReason failed", e); } - } - private void setMaxParallelDownloadsInternal(int maxParallelDownloads) { - maxParallelDownloadsInternal = maxParallelDownloads; - // TODO: Start or stop downloads if necessary. - } + private void setMaxParallelDownloads(int maxParallelDownloads) { + this.maxParallelDownloads = maxParallelDownloads; + // TODO: Start or stop downloads if necessary. + } - private void setMinRetryCountInternal(int minRetryCount) { - minRetryCountInternal = minRetryCount; - } + private void setMinRetryCount(int minRetryCount) { + this.minRetryCount = minRetryCount; + } - private void addDownloadInternal(DownloadRequest request, int stopReason) { - DownloadInternal downloadInternal = getDownload(request.id); - if (downloadInternal != null) { - downloadInternal.addRequest(request, stopReason); - logd("Request is added to existing download", downloadInternal); - } else { - Download download = loadDownload(request.id); - if (download == null) { - long nowMs = System.currentTimeMillis(); - download = - new Download( - request, - stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs, - /* contentLength= */ C.LENGTH_UNSET, - stopReason, - Download.FAILURE_REASON_NONE); - logd("Download state is created for " + request.id); + private void addDownload(DownloadRequest request, int stopReason) { + DownloadInternal downloadInternal = getDownload(request.id); + if (downloadInternal != null) { + downloadInternal.addRequest(request, stopReason); + logd("Request is added to existing download", downloadInternal); } else { - download = mergeRequest(download, request, stopReason); - logd("Download state is loaded for " + request.id); + Download download = loadDownload(request.id); + if (download == null) { + long nowMs = System.currentTimeMillis(); + download = + new Download( + request, + stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + Download.FAILURE_REASON_NONE); + logd("Download state is created for " + request.id); + } else { + download = mergeRequest(download, request, stopReason); + logd("Download state is loaded for " + request.id); + } + addDownloadForState(download); } - addDownloadForState(download); } - } - private void removeDownloadInternal(String id) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - downloadInternal.remove(); - } else { - Download download = loadDownload(id); - if (download != null) { - addDownloadForState(copyWithState(download, STATE_REMOVING)); + private void removeDownload(String id) { + DownloadInternal downloadInternal = getDownload(id); + if (downloadInternal != null) { + downloadInternal.remove(); } else { - logd("Can't remove download. No download with id: " + id); + Download download = loadDownload(id); + if (download != null) { + addDownloadForState(copyWithState(download, STATE_REMOVING)); + } else { + logd("Can't remove download. No download with id: " + id); + } } } - } - private void onDownloadThreadStoppedInternal(DownloadThread downloadThread) { - logd("Download is stopped", downloadThread.request); - String downloadId = downloadThread.request.id; - downloadThreads.remove(downloadId); - boolean tryToStartDownloads = false; - if (!downloadThread.isRemove) { - // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloadsInternal; - parallelDownloads--; - } - getDownload(downloadId) - .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); - if (tryToStartDownloads) { - for (int i = 0; - parallelDownloads < maxParallelDownloadsInternal && i < downloadInternals.size(); - i++) { - downloadInternals.get(i).start(); + private void onDownloadThreadStopped(DownloadThread downloadThread) { + logd("Download is stopped", downloadThread.request); + String downloadId = downloadThread.request.id; + downloadThreads.remove(downloadId); + boolean tryToStartDownloads = false; + if (!downloadThread.isRemove) { + // If maxParallelDownloads was hit, there might be a download waiting for a slot. + tryToStartDownloads = parallelDownloads == maxParallelDownloads; + parallelDownloads--; + } + getDownload(downloadId) + .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); + if (tryToStartDownloads) { + for (int i = 0; + parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); + i++) { + downloadInternals.get(i).start(); + } } } - } - - private void onDownloadThreadContentLengthChangedInternal(DownloadThread downloadThread) { - String downloadId = downloadThread.request.id; - getDownload(downloadId).setContentLength(downloadThread.contentLength); - } - - private void releaseInternal() { - for (DownloadThread downloadThread : downloadThreads.values()) { - downloadThread.cancel(/* released= */ true); - } - downloadThreads.clear(); - downloadInternals.clear(); - internalThread.quit(); - synchronized (releaseLock) { - released = true; - releaseLock.notifyAll(); - } - } - - private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { - logd("Download state is changed", downloadInternal); - try { - downloadIndex.putDownload(download); - } catch (IOException e) { - Log.e(TAG, "Failed to update index", e); - } - if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { - downloadInternals.remove(downloadInternal); - } - mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); - } - private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { - logd("Download is removed", downloadInternal); - try { - downloadIndex.removeDownload(download.request.id); - } catch (IOException e) { - Log.e(TAG, "Failed to remove from index", e); + private void onDownloadThreadContentLengthChanged(DownloadThread downloadThread) { + String downloadId = downloadThread.request.id; + getDownload(downloadId).setContentLength(downloadThread.contentLength); } - downloadInternals.remove(downloadInternal); - mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); - } - @StartThreadResults - private int startDownloadThread(DownloadInternal downloadInternal) { - DownloadRequest request = downloadInternal.download.request; - String downloadId = request.id; - if (downloadThreads.containsKey(downloadId)) { - if (stopDownloadThreadInternal(downloadId)) { - return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; + private void release() { + for (DownloadThread downloadThread : downloadThreads.values()) { + downloadThread.cancel(/* released= */ true); } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; - } - boolean isRemove = downloadInternal.isInRemoveState(); - if (!isRemove) { - if (parallelDownloads == maxParallelDownloadsInternal) { - return START_THREAD_TOO_MANY_DOWNLOADS; + downloadThreads.clear(); + downloadInternals.clear(); + thread.quit(); + synchronized (this) { + released = true; + notifyAll(); } - parallelDownloads++; } - Downloader downloader = downloaderFactory.createDownloader(request); - DownloadProgress downloadProgress = downloadInternal.download.progress; - DownloadThread downloadThread = - new DownloadThread( - request, - downloader, - downloadProgress, - isRemove, - minRetryCountInternal, - internalHandler); - downloadThreads.put(downloadId, downloadThread); - downloadThread.start(); - logd("Download is started", downloadInternal); - return START_THREAD_SUCCEEDED; - } - private boolean stopDownloadThreadInternal(String downloadId) { - DownloadThread downloadThread = downloadThreads.get(downloadId); - if (downloadThread != null && !downloadThread.isRemove) { - downloadThread.cancel(/* released= */ false); - logd("Download is cancelled", downloadThread.request); - return true; + private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { + logd("Download state is changed", downloadInternal); + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index", e); + } + if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { + downloadInternals.remove(downloadInternal); + } + mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); } - return false; - } - @Nullable - private DownloadInternal getDownload(String id) { - for (int i = 0; i < downloadInternals.size(); i++) { - DownloadInternal downloadInternal = downloadInternals.get(i); - if (downloadInternal.download.request.id.equals(id)) { - return downloadInternal; + private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { + logd("Download is removed", downloadInternal); + try { + downloadIndex.removeDownload(download.request.id); + } catch (IOException e) { + Log.e(TAG, "Failed to remove from index", e); } + downloadInternals.remove(downloadInternal); + mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); } - return null; - } - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); + @StartThreadResults + private int startDownloadThread(DownloadInternal downloadInternal) { + DownloadRequest request = downloadInternal.download.request; + String downloadId = request.id; + if (downloadThreads.containsKey(downloadId)) { + if (stopDownloadThreadInternal(downloadId)) { + return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; + } + return START_THREAD_WAIT_REMOVAL_TO_FINISH; + } + boolean isRemove = downloadInternal.isInRemoveState(); + if (!isRemove) { + if (parallelDownloads == maxParallelDownloads) { + return START_THREAD_TOO_MANY_DOWNLOADS; + } + parallelDownloads++; + } + Downloader downloader = downloaderFactory.createDownloader(request); + DownloadProgress downloadProgress = downloadInternal.download.progress; + DownloadThread downloadThread = + new DownloadThread(request, downloader, downloadProgress, isRemove, minRetryCount, this); + downloadThreads.put(downloadId, downloadThread); + downloadThread.start(); + logd("Download is started", downloadInternal); + return START_THREAD_SUCCEEDED; + } + + private boolean stopDownloadThreadInternal(String downloadId) { + DownloadThread downloadThread = downloadThreads.get(downloadId); + if (downloadThread != null && !downloadThread.isRemove) { + downloadThread.cancel(/* released= */ false); + logd("Download is cancelled", downloadThread.request); + return true; + } + return false; } - return null; - } - - private void addDownloadForState(Download download) { - DownloadInternal downloadInternal = new DownloadInternal(this, download); - downloadInternals.add(downloadInternal); - logd("Download is added", downloadInternal); - downloadInternal.initialize(); - } - - private boolean canStartDownloads() { - return !downloadsPausedInternal && notMetRequirements == 0; - } - /* package */ static Download mergeRequest( - Download download, DownloadRequest request, int stopReason) { - @Download.State int state = download.state; - if (state == STATE_REMOVING || state == STATE_RESTARTING) { - state = STATE_RESTARTING; - } else if (stopReason != STOP_REASON_NONE) { - state = STATE_STOPPED; - } else { - state = STATE_QUEUED; + @Nullable + private DownloadInternal getDownload(String id) { + for (int i = 0; i < downloadInternals.size(); i++) { + DownloadInternal downloadInternal = downloadInternals.get(i); + if (downloadInternal.download.request.id.equals(id)) { + return downloadInternal; + } + } + return null; } - long nowMs = System.currentTimeMillis(); - long startTimeMs = download.isTerminalState() ? nowMs : download.startTimeMs; - return new Download( - download.request.copyWithMergedRequest(request), - state, - startTimeMs, - /* updateTimeMs= */ nowMs, - /* contentLength= */ C.LENGTH_UNSET, - stopReason, - FAILURE_REASON_NONE); - } - private static Download copyWithState(Download download, @Download.State int state) { - return new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - download.stopReason, - FAILURE_REASON_NONE, - download.progress); - } - - private static void logd(String message) { - if (DEBUG) { - Log.d(TAG, message); + private Download loadDownload(String id) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "loadDownload failed", e); + } + return null; } - } - private static void logd(String message, DownloadInternal downloadInternal) { - logd(message, downloadInternal.download.request); - } - - private static void logd(String message, DownloadRequest request) { - if (DEBUG) { - logd(message + ": " + request); + private void addDownloadForState(Download download) { + DownloadInternal downloadInternal = new DownloadInternal(this, download); + downloadInternals.add(downloadInternal); + logd("Download is added", downloadInternal); + downloadInternal.initialize(); } - } - private static void logdFlags(String message, int flags) { - if (DEBUG) { - logd(message + ": " + Integer.toBinaryString(flags)); + private boolean canStartDownloads() { + return !downloadsPaused && notMetRequirements == 0; } } private static final class DownloadInternal { - private final DownloadManager downloadManager; + private final InternalHandler internalHandler; private Download download; @@ -967,8 +976,8 @@ private static final class DownloadInternal { private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; - private DownloadInternal(DownloadManager downloadManager, Download download) { - this.downloadManager = downloadManager; + private DownloadInternal(InternalHandler internalHandler, Download download) { + this.internalHandler = internalHandler; this.download = download; state = download.state; contentLength = download.contentLength; @@ -1016,7 +1025,7 @@ public void start() { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } } @@ -1034,7 +1043,7 @@ public void setContentLength(long contentLength) { return; } this.contentLength = contentLength; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } private void updateStopState() { @@ -1045,12 +1054,12 @@ private void updateStopState() { } } else { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - downloadManager.stopDownloadThreadInternal(download.request.id); + internalHandler.stopDownloadThreadInternal(download.request.id); setState(STATE_STOPPED); } } if (oldDownload == download) { - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } @@ -1059,24 +1068,24 @@ private void initialize(int initialState) { // state immediately. state = initialState; if (isInRemoveState()) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } else if (canStart()) { startOrQueue(); } else { setState(STATE_STOPPED); } if (state == initialState) { - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } private boolean canStart() { - return downloadManager.canStartDownloads() && stopReason == STOP_REASON_NONE; + return internalHandler.canStartDownloads() && stopReason == STOP_REASON_NONE; } private void startOrQueue() { Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = downloadManager.startDownloadThread(this); + @StartThreadResults int result = internalHandler.startDownloadThread(this); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { setState(STATE_DOWNLOADING); @@ -1088,7 +1097,7 @@ private void startOrQueue() { private void setState(@Download.State int newState) { if (state != newState) { state = newState; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } @@ -1097,9 +1106,9 @@ private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable err return; } if (isCanceled) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } else if (state == STATE_REMOVING) { - downloadManager.onDownloadRemovedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadRemovedInternal(this, getUpdatedDownload()); } else if (state == STATE_RESTARTING) { initialize(STATE_QUEUED); } else { // STATE_DOWNLOADING @@ -1122,7 +1131,7 @@ private static class DownloadThread extends Thread implements Downloader.Progres private final boolean isRemove; private final int minRetryCount; - private volatile Handler updateHandler; + private volatile InternalHandler internalHandler; private volatile boolean isCanceled; private Throwable finalError; @@ -1134,13 +1143,13 @@ private DownloadThread( DownloadProgress downloadProgress, boolean isRemove, int minRetryCount, - Handler updateHandler) { + InternalHandler internalHandler) { this.request = request; this.downloader = downloader; this.downloadProgress = downloadProgress; this.isRemove = isRemove; this.minRetryCount = minRetryCount; - this.updateHandler = updateHandler; + this.internalHandler = internalHandler; contentLength = C.LENGTH_UNSET; } @@ -1150,7 +1159,7 @@ public void cancel(boolean released) { // cancellation to complete depends on the implementation of the downloader being used. We // null the handler reference here so that it doesn't prevent garbage collection of the // download manager whilst cancellation is ongoing. - updateHandler = null; + internalHandler = null; } isCanceled = true; downloader.cancel(); @@ -1192,9 +1201,9 @@ public void run() { } catch (Throwable e) { finalError = e; } - Handler updateHandler = this.updateHandler; - if (updateHandler != null) { - updateHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); } } @@ -1204,9 +1213,9 @@ public void onProgress(long contentLength, long bytesDownloaded, float percentDo downloadProgress.percentDownloaded = percentDownloaded; if (contentLength != this.contentLength) { this.contentLength = contentLength; - Handler updateHandler = this.updateHandler; - if (updateHandler != null) { - updateHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); } } } From b55e17588b2328c77a33586e5c81cd9413ff6201 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Apr 2019 14:35:51 +0100 Subject: [PATCH 038/219] Link blog post from release notes PiperOrigin-RevId: 245411528 --- RELEASENOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 342ca55cc99..0beec1ef81d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,8 +3,10 @@ ### 2.10.0 ### * Core library: - * Improve decoder re-use between playbacks. TODO: Write and link a blog post - here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. * Fix issue where using `ProgressiveMediaSource.Factory` would mean that `DefaultExtractorsFactory` would be kept by proguard. Custom From f62fa434dd8513a1766e688e59febb258762e968 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Apr 2019 18:14:55 +0100 Subject: [PATCH 039/219] Log warnings when extension libraries can't be used Issue: #5788 PiperOrigin-RevId: 245440858 --- RELEASENOTES.md | 5 +++++ .../android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 12 +++++++++++- .../android/exoplayer2/util/LibraryLoader.java | 8 +++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0beec1ef81d..bb612ea319d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -115,6 +115,11 @@ order when in shuffle mode. * Allow handling of custom commands via `registerCustomCommandReceiver`. * Add ability to include an extras `Bundle` when reporting a custom error. +* LoadControl: Set minimum buffer for playbacks with video equal to maximum + buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)). +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). ### 2.9.6 ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index bc36fc4f3b6..58109c16660 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -30,6 +31,8 @@ public final class FfmpegLibrary { ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg"); } + private static final String TAG = "FfmpegLibrary"; + private static final LibraryLoader LOADER = new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); @@ -69,7 +72,14 @@ public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encodin return false; } String codecName = getCodecName(mimeType, encoding); - return codecName != null && ffmpegHasDecoder(codecName); + if (codecName == null) { + return false; + } + if (!ffmpegHasDecoder(codecName)) { + Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration."); + return false; + } + return true; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java index c12bae0a073..7ee88d8f0fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java @@ -15,11 +15,15 @@ */ package com.google.android.exoplayer2.util; +import java.util.Arrays; + /** * Configurable loader for native libraries. */ public final class LibraryLoader { + private static final String TAG = "LibraryLoader"; + private String[] nativeLibraries; private boolean loadAttempted; private boolean isAvailable; @@ -54,7 +58,9 @@ public synchronized boolean isAvailable() { } isAvailable = true; } catch (UnsatisfiedLinkError exception) { - // Do nothing. + // Log a warning as an attempt to check for the library indicates that the app depends on an + // extension and generally would expect its native libraries to be available. + Log.w(TAG, "Failed to load " + Arrays.toString(nativeLibraries)); } return isAvailable; } From 9463c31cded5cd6523769c4deab04cc4204eeb3c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Apr 2019 11:00:55 +0100 Subject: [PATCH 040/219] Update default min duration for playbacks with video to match max duration. Experiments show this is beneficial for rebuffers with only minor impact on battery usage. Configurations which explicitly set a minimum buffer duration are unaffected. Issue:#2083 PiperOrigin-RevId: 244823642 --- .../exoplayer2/DefaultLoadControl.java | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 83cb5b723c1..972f651a41a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -29,12 +29,14 @@ public class DefaultLoadControl implements LoadControl { /** * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. + * times, in milliseconds. This value is only applied to playbacks without video. */ public static final int DEFAULT_MIN_BUFFER_MS = 15000; /** * The default maximum duration of media that the player will attempt to buffer, in milliseconds. + * For playbacks with video, this is also the default minimum duration of media that the player + * will attempt to ensure is buffered. */ public static final int DEFAULT_MAX_BUFFER_MS = 50000; @@ -69,7 +71,8 @@ public class DefaultLoadControl implements LoadControl { public static final class Builder { private DefaultAllocator allocator; - private int minBufferMs; + private int minBufferAudioMs; + private int minBufferVideoMs; private int maxBufferMs; private int bufferForPlaybackMs; private int bufferForPlaybackAfterRebufferMs; @@ -81,7 +84,8 @@ public static final class Builder { /** Constructs a new instance. */ public Builder() { - minBufferMs = DEFAULT_MIN_BUFFER_MS; + minBufferAudioMs = DEFAULT_MIN_BUFFER_MS; + minBufferVideoMs = DEFAULT_MAX_BUFFER_MS; maxBufferMs = DEFAULT_MAX_BUFFER_MS; bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; @@ -125,7 +129,18 @@ public Builder setBufferDurationsMs( int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs) { Assertions.checkState(!createDefaultLoadControlCalled); - this.minBufferMs = minBufferMs; + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual( + bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferMs, + bufferForPlaybackAfterRebufferMs, + "minBufferMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + this.minBufferAudioMs = minBufferMs; + this.minBufferVideoMs = minBufferMs; this.maxBufferMs = maxBufferMs; this.bufferForPlaybackMs = bufferForPlaybackMs; this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; @@ -173,6 +188,7 @@ public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSiz */ public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { Assertions.checkState(!createDefaultLoadControlCalled); + assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.backBufferDurationMs = backBufferDurationMs; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; return this; @@ -187,7 +203,8 @@ public DefaultLoadControl createDefaultLoadControl() { } return new DefaultLoadControl( allocator, - minBufferMs, + minBufferAudioMs, + minBufferVideoMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -200,7 +217,8 @@ public DefaultLoadControl createDefaultLoadControl() { private final DefaultAllocator allocator; - private final long minBufferUs; + private final long minBufferAudioUs; + private final long minBufferVideoUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; @@ -211,6 +229,7 @@ public DefaultLoadControl createDefaultLoadControl() { private int targetBufferSize; private boolean isBuffering; + private boolean hasVideo; /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ @SuppressWarnings("deprecation") @@ -220,16 +239,18 @@ public DefaultLoadControl() { /** @deprecated Use {@link Builder} instead. */ @Deprecated - @SuppressWarnings("deprecation") public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, - DEFAULT_MIN_BUFFER_MS, + /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS, + /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_TARGET_BUFFER_BYTES, - DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS, + DEFAULT_BACK_BUFFER_DURATION_MS, + DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); } /** @deprecated Use {@link Builder} instead. */ @@ -244,7 +265,8 @@ public DefaultLoadControl( boolean prioritizeTimeOverSizeThresholds) { this( allocator, - minBufferMs, + /* minBufferAudioMs= */ minBufferMs, + /* minBufferVideoMs= */ minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -256,7 +278,8 @@ public DefaultLoadControl( protected DefaultLoadControl( DefaultAllocator allocator, - int minBufferMs, + int minBufferAudioMs, + int minBufferVideoMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, @@ -267,17 +290,27 @@ protected DefaultLoadControl( assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); assertGreaterOrEqual( bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); - assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); assertGreaterOrEqual( - minBufferMs, + minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferAudioMs, bufferForPlaybackAfterRebufferMs, - "minBufferMs", + "minBufferAudioMs", "bufferForPlaybackAfterRebufferMs"); - assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + assertGreaterOrEqual( + minBufferVideoMs, + bufferForPlaybackAfterRebufferMs, + "minBufferVideoMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs"); + assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs"); assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.allocator = allocator; - this.minBufferUs = C.msToUs(minBufferMs); + this.minBufferAudioUs = C.msToUs(minBufferAudioMs); + this.minBufferVideoUs = C.msToUs(minBufferVideoMs); this.maxBufferUs = C.msToUs(maxBufferMs); this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); @@ -295,6 +328,7 @@ public void onPrepared() { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + hasVideo = hasVideo(renderers, trackSelections); targetBufferSize = targetBufferBytesOverwrite == C.LENGTH_UNSET ? calculateTargetBufferSize(renderers, trackSelections) @@ -330,7 +364,7 @@ public boolean retainBackBufferFromKeyframe() { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - long minBufferUs = this.minBufferUs; + long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media // duration to keep enough media buffered for a playout duration of minBufferUs. @@ -384,6 +418,15 @@ private void reset(boolean resetAllocator) { } } + private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) { + return true; + } + } + return false; + } + private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); } From e4f1f89f5ca4a21c064f34d70b76a4094fb42cb9 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 18:26:24 +0100 Subject: [PATCH 041/219] Downloading documentation PiperOrigin-RevId: 245443109 --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bb612ea319d..80650974e79 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,7 +23,8 @@ ([#5520](https://github.com/google/ExoPlayer/issues/5520)). * Offline: * Improve offline support. `DownloadManager` now tracks all offline content, - not just tasks in progress. TODO: Write and link a blog post here. + not just tasks in progress. Read [this page](https://exoplayer.dev/downloading-media.html) for + more details. * Caching: * Improve performance of `SimpleCache` ([#4253](https://github.com/google/ExoPlayer/issues/4253)). From 0128cebce1e1cb04c5a4974f25006354579fe286 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Apr 2019 12:05:09 +0100 Subject: [PATCH 042/219] Add simpler DownloadManager constructor PiperOrigin-RevId: 245397736 --- .../exoplayer2/offline/DownloadManager.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 aa0cd12231d..2caf89155a5 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 @@ -193,6 +193,24 @@ public DownloadManager( new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); } + /** + * Constructs a {@link DownloadManager}. + * + * @param context Any context. + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param cache A cache to be used to store downloaded data. The cache should be configured with + * an {@link CacheEvictor} that will not evict downloaded content, for example {@link + * NoOpCacheEvictor}. + * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data. + */ + public DownloadManager( + Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) { + this( + context, + new DefaultDownloadIndex(databaseProvider), + new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); + } + /** * Constructs a {@link DownloadManager}. * From 5eb36f86a25a3f988771ad38fd746f3b9bd25c66 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 26 Apr 2019 18:49:45 +0100 Subject: [PATCH 043/219] Fix line break --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80650974e79..aac46647cc2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,8 +23,8 @@ ([#5520](https://github.com/google/ExoPlayer/issues/5520)). * Offline: * Improve offline support. `DownloadManager` now tracks all offline content, - not just tasks in progress. Read [this page](https://exoplayer.dev/downloading-media.html) for - more details. + not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more details. * Caching: * Improve performance of `SimpleCache` ([#4253](https://github.com/google/ExoPlayer/issues/4253)). From 590140c1a6c7b48d968c71c9157bd1ea00c5a849 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 26 Apr 2019 20:41:29 +0100 Subject: [PATCH 044/219] Fix bad merge --- .../exoplayer2/offline/DownloadManager.java | 18 ------------------ 1 file changed, 18 deletions(-) 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 2caf89155a5..aa0cd12231d 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 @@ -193,24 +193,6 @@ public DownloadManager( new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); } - /** - * Constructs a {@link DownloadManager}. - * - * @param context Any context. - * @param databaseProvider Provides the SQLite database in which downloads are persisted. - * @param cache A cache to be used to store downloaded data. The cache should be configured with - * an {@link CacheEvictor} that will not evict downloaded content, for example {@link - * NoOpCacheEvictor}. - * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data. - */ - public DownloadManager( - Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) { - this( - context, - new DefaultDownloadIndex(databaseProvider), - new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); - } - /** * Constructs a {@link DownloadManager}. * From 618d97db1c6fbb917740ed53848fc120cad957d1 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 29 Apr 2019 15:56:41 +0100 Subject: [PATCH 045/219] Never set null as a session meta data object. Issue: #5810 PiperOrigin-RevId: 245745646 --- .../ext/mediasession/MediaSessionConnector.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 24cf4062f76..9c80fabc50a 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -146,6 +146,9 @@ public final class MediaSessionConnector { private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; + private static final MediaMetadataCompat METADATA_EMPTY = + new MediaMetadataCompat.Builder().build(); + /** Receiver of media commands sent by a media controller. */ public interface CommandReceiver { /** @@ -639,8 +642,8 @@ public final void invalidateMediaSessionMetadata() { MediaMetadataCompat metadata = mediaMetadataProvider != null && player != null ? mediaMetadataProvider.getMetadata(player) - : null; - mediaSession.setMetadata(metadata); + : METADATA_EMPTY; + mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY); } /** @@ -888,7 +891,7 @@ public DefaultMediaMetadataProvider( @Override public MediaMetadataCompat getMetadata(Player player) { if (player.getCurrentTimeline().isEmpty()) { - return null; + return METADATA_EMPTY; } MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); if (player.isPlayingAd()) { From 6b34ade908dfe750f6d26c2ca74636a542556ac3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Apr 2019 10:56:23 +0100 Subject: [PATCH 046/219] Rename DownloadThread to Task This resolves some naming confusion that previously existed as a result of DownloadThread also being used for removals. Some related variables (e.g. activeDownloadCount) would refer to both download and removal tasks, whilst others (e.g. maxParallelDownloads) would refer only to downloads. This change renames those that refer to both to use "task" terminology. This change also includes minor test edits. PiperOrigin-RevId: 245913671 --- .../exoplayer2/offline/DownloadManager.java | 121 +++++++++--------- .../exoplayer2/offline/DownloadBuilder.java | 4 +- .../offline/DownloadManagerTest.java | 4 +- 3 files changed, 68 insertions(+), 61 deletions(-) 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 aa0cd12231d..7ad22e000ab 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 @@ -137,7 +137,7 @@ default void onRequirementsStateChanged( 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_DOWNLOAD_THREAD_STOPPED = 8; + private static final int MSG_TASK_STOPPED = 8; private static final int MSG_CONTENT_LENGTH_CHANGED = 9; private static final int MSG_RELEASE = 10; @@ -168,7 +168,7 @@ default void onRequirementsStateChanged( private final ArrayList downloads; private int pendingMessages; - private int activeDownloadCount; + private int activeTaskCount; private boolean initialized; private boolean downloadsPaused; private int maxParallelDownloads; @@ -244,7 +244,7 @@ public boolean isInitialized() { * download requirements are not met). */ public boolean isIdle() { - return activeDownloadCount == 0 && pendingMessages == 0; + return activeTaskCount == 0 && pendingMessages == 0; } /** @@ -465,7 +465,7 @@ public void release() { mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. pendingMessages = 0; - activeDownloadCount = 0; + activeTaskCount = 0; initialized = false; downloads.clear(); } @@ -503,8 +503,8 @@ private boolean handleMainMessage(Message message) { break; case MSG_PROCESSED: int processedMessageCount = message.arg1; - int activeDownloadCount = message.arg2; - onMessageProcessed(processedMessageCount, activeDownloadCount); + int activeTaskCount = message.arg2; + onMessageProcessed(processedMessageCount, activeTaskCount); break; default: throw new IllegalStateException(); @@ -543,9 +543,9 @@ private void onDownloadRemoved(Download download) { } } - private void onMessageProcessed(int processedMessageCount, int activeDownloadCount) { + private void onMessageProcessed(int processedMessageCount, int activeTaskCount) { this.pendingMessages -= processedMessageCount; - this.activeDownloadCount = activeDownloadCount; + this.activeTaskCount = activeTaskCount; if (isIdle()) { for (Listener listener : listeners) { listener.onIdle(this); @@ -627,7 +627,7 @@ private static final class InternalHandler extends Handler { private final DownloaderFactory downloaderFactory; private final Handler mainHandler; private final ArrayList downloadInternals; - private final HashMap downloadThreads; + private final HashMap activeTasks; // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; @@ -653,7 +653,7 @@ public InternalHandler( this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; downloadInternals = new ArrayList<>(); - downloadThreads = new HashMap<>(); + activeTasks = new HashMap<>(); } @Override @@ -694,14 +694,14 @@ public void handleMessage(Message message) { id = (String) message.obj; removeDownload(id); break; - case MSG_DOWNLOAD_THREAD_STOPPED: - DownloadThread downloadThread = (DownloadThread) message.obj; - onDownloadThreadStopped(downloadThread); + case MSG_TASK_STOPPED: + Task task = (Task) message.obj; + onTaskStopped(task); processedExternalMessage = false; // This message is posted internally. break; case MSG_CONTENT_LENGTH_CHANGED: - downloadThread = (DownloadThread) message.obj; - onDownloadThreadContentLengthChanged(downloadThread); + task = (Task) message.obj; + onContentLengthChanged(task); processedExternalMessage = false; // This message is posted internally. break; case MSG_RELEASE: @@ -711,7 +711,7 @@ public void handleMessage(Message message) { throw new IllegalStateException(); } mainHandler - .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) + .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, activeTasks.size()) .sendToTarget(); } @@ -832,18 +832,17 @@ private void removeDownload(String id) { } } - private void onDownloadThreadStopped(DownloadThread downloadThread) { - logd("Download is stopped", downloadThread.request); - String downloadId = downloadThread.request.id; - downloadThreads.remove(downloadId); + private void onTaskStopped(Task task) { + logd("Task is stopped", task.request); + String downloadId = task.request.id; + activeTasks.remove(downloadId); boolean tryToStartDownloads = false; - if (!downloadThread.isRemove) { + if (!task.isRemove) { // If maxParallelDownloads was hit, there might be a download waiting for a slot. tryToStartDownloads = parallelDownloads == maxParallelDownloads; parallelDownloads--; } - getDownload(downloadId) - .onDownloadThreadStopped(downloadThread.isCanceled, downloadThread.finalError); + getDownload(downloadId).onTaskStopped(task.isCanceled, task.finalError); if (tryToStartDownloads) { for (int i = 0; parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); @@ -853,16 +852,16 @@ private void onDownloadThreadStopped(DownloadThread downloadThread) { } } - private void onDownloadThreadContentLengthChanged(DownloadThread downloadThread) { - String downloadId = downloadThread.request.id; - getDownload(downloadId).setContentLength(downloadThread.contentLength); + private void onContentLengthChanged(Task task) { + String downloadId = task.request.id; + getDownload(downloadId).setContentLength(task.contentLength); } private void release() { - for (DownloadThread downloadThread : downloadThreads.values()) { - downloadThread.cancel(/* released= */ true); + for (Task task : activeTasks.values()) { + task.cancel(/* released= */ true); } - downloadThreads.clear(); + activeTasks.clear(); downloadInternals.clear(); thread.quit(); synchronized (this) { @@ -871,7 +870,7 @@ private void release() { } } - private void onDownloadChangedInternal(DownloadInternal downloadInternal, Download download) { + private void onDownloadChanged(DownloadInternal downloadInternal, Download download) { logd("Download state is changed", downloadInternal); try { downloadIndex.putDownload(download); @@ -884,7 +883,7 @@ private void onDownloadChangedInternal(DownloadInternal downloadInternal, Downlo mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); } - private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Download download) { + private void onDownloadRemoved(DownloadInternal downloadInternal, Download download) { logd("Download is removed", downloadInternal); try { downloadIndex.removeDownload(download.request.id); @@ -896,11 +895,11 @@ private void onDownloadRemovedInternal(DownloadInternal downloadInternal, Downlo } @StartThreadResults - private int startDownloadThread(DownloadInternal downloadInternal) { + private int startTask(DownloadInternal downloadInternal) { DownloadRequest request = downloadInternal.download.request; String downloadId = request.id; - if (downloadThreads.containsKey(downloadId)) { - if (stopDownloadThreadInternal(downloadId)) { + if (activeTasks.containsKey(downloadId)) { + if (stopDownloadTask(downloadId)) { return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; } return START_THREAD_WAIT_REMOVAL_TO_FINISH; @@ -914,19 +913,25 @@ private int startDownloadThread(DownloadInternal downloadInternal) { } Downloader downloader = downloaderFactory.createDownloader(request); DownloadProgress downloadProgress = downloadInternal.download.progress; - DownloadThread downloadThread = - new DownloadThread(request, downloader, downloadProgress, isRemove, minRetryCount, this); - downloadThreads.put(downloadId, downloadThread); - downloadThread.start(); - logd("Download is started", downloadInternal); + Task task = + new Task( + request, + downloader, + downloadProgress, + isRemove, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(downloadId, task); + task.start(); + logd("Task is started", downloadInternal); return START_THREAD_SUCCEEDED; } - private boolean stopDownloadThreadInternal(String downloadId) { - DownloadThread downloadThread = downloadThreads.get(downloadId); - if (downloadThread != null && !downloadThread.isRemove) { - downloadThread.cancel(/* released= */ false); - logd("Download is cancelled", downloadThread.request); + private boolean stopDownloadTask(String downloadId) { + Task task = activeTasks.get(downloadId); + if (task != null && !task.isRemove) { + task.cancel(/* released= */ false); + logd("Task is cancelled", task.request); return true; } return false; @@ -1025,7 +1030,7 @@ public void start() { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } } @@ -1043,7 +1048,7 @@ public void setContentLength(long contentLength) { return; } this.contentLength = contentLength; - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } private void updateStopState() { @@ -1054,12 +1059,12 @@ private void updateStopState() { } } else { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - internalHandler.stopDownloadThreadInternal(download.request.id); + internalHandler.stopDownloadTask(download.request.id); setState(STATE_STOPPED); } } if (oldDownload == download) { - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } @@ -1068,14 +1073,14 @@ private void initialize(int initialState) { // state immediately. state = initialState; if (isInRemoveState()) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } else if (canStart()) { startOrQueue(); } else { setState(STATE_STOPPED); } if (state == initialState) { - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } @@ -1085,7 +1090,7 @@ private boolean canStart() { private void startOrQueue() { Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = internalHandler.startDownloadThread(this); + @StartThreadResults int result = internalHandler.startTask(this); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { setState(STATE_DOWNLOADING); @@ -1097,18 +1102,18 @@ private void startOrQueue() { private void setState(@Download.State int newState) { if (state != newState) { state = newState; - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } } - private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) { + private void onTaskStopped(boolean isCanceled, @Nullable Throwable error) { if (isIdle()) { return; } if (isCanceled) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } else if (state == STATE_REMOVING) { - internalHandler.onDownloadRemovedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadRemoved(this, getUpdatedDownload()); } else if (state == STATE_RESTARTING) { initialize(STATE_QUEUED); } else { // STATE_DOWNLOADING @@ -1123,7 +1128,7 @@ private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable err } } - private static class DownloadThread extends Thread implements Downloader.ProgressListener { + private static class Task extends Thread implements Downloader.ProgressListener { private final DownloadRequest request; private final Downloader downloader; @@ -1137,7 +1142,7 @@ private static class DownloadThread extends Thread implements Downloader.Progres private long contentLength; - private DownloadThread( + private Task( DownloadRequest request, Downloader downloader, DownloadProgress downloadProgress, @@ -1203,7 +1208,7 @@ public void run() { } Handler internalHandler = this.internalHandler; if (internalHandler != null) { - internalHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java index f901b00f532..e07166a21cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java @@ -40,7 +40,7 @@ @Nullable private String cacheKey; private byte[] customMetadata; - private int state; + @Download.State private int state; private long startTimeMs; private long updateTimeMs; private long contentLength; @@ -111,7 +111,7 @@ public DownloadBuilder setCacheKey(@Nullable String cacheKey) { return this; } - public DownloadBuilder setState(int state) { + public DownloadBuilder setState(@Download.State int state) { this.state = state; return this; } 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 5798e9df8c3..92c6debdd8d 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 @@ -359,7 +359,7 @@ public void getTasks_returnTasks() { } @Test - public void stopAndResume() throws Throwable { + public void pauseAndResume() throws Throwable { DownloadRunner runner1 = new DownloadRunner(uri1); DownloadRunner runner2 = new DownloadRunner(uri2); DownloadRunner runner3 = new DownloadRunner(uri3); @@ -370,10 +370,12 @@ public void stopAndResume() throws Throwable { runOnMainThread(() -> downloadManager.pauseDownloads()); + // TODO: This should be assertQueued. Fix implementation and update test. runner1.getTask().assertStopped(); // remove requests aren't stopped. runner2.getDownloader(1).unblock().assertReleased(); + // TODO: This should be assertQueued. Fix implementation and update test. runner2.getTask().assertStopped(); // Although remove2 is finished, download2 doesn't start. runner2.getDownloader(2).assertDoesNotStart(); From 4a5b8e17de84d9ae34af3f0964147f9a2bffcd49 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Apr 2019 12:25:04 +0100 Subject: [PATCH 047/219] DownloadManager improvements - Do requirements TODO - Add useful helper method to retrieve not met requirements - Fix WritableDownloadIndex Javadoc PiperOrigin-RevId: 245922903 --- .../exoplayer2/offline/DownloadManager.java | 25 +++++++++++++----- .../offline/WritableDownloadIndex.java | 26 +++++++++---------- 2 files changed, 31 insertions(+), 20 deletions(-) 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 7ad22e000ab..8502a56ea73 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 @@ -173,6 +173,7 @@ default void onRequirementsStateChanged( private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; + private int notMetRequirements; private RequirementsWatcher requirementsWatcher; /** @@ -212,7 +213,7 @@ public DownloadManager( requirementsListener = this::onRequirementsStateChanged; requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); - int notMetRequirements = requirementsWatcher.start(); + notMetRequirements = requirementsWatcher.start(); mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); @@ -274,11 +275,21 @@ public void removeListener(Listener listener) { listeners.remove(listener); } - /** Returns the requirements needed to be met to start downloads. */ + /** Returns the requirements needed to be met to progress. */ public Requirements getRequirements() { return requirementsWatcher.getRequirements(); } + /** + * Returns the requirements needed for downloads to progress that are not currently met. + * + * @return The not met {@link Requirements.RequirementFlags}, or 0 if all requirements are met. + */ + @Requirements.RequirementFlags + public int getNotMetRequirements() { + return getRequirements().getNotMetRequirements(context); + } + /** * Sets the requirements that need to be met for downloads to progress. * @@ -413,7 +424,7 @@ public void setStopReason(@Nullable String id, int stopReason) { * @param request The download request. */ public void addDownload(DownloadRequest request) { - addDownload(request, Download.STOP_REASON_NONE); + addDownload(request, STOP_REASON_NONE); } /** @@ -478,6 +489,10 @@ private void onRequirementsStateChanged( for (Listener listener : listeners) { listener.onRequirementsStateChanged(this, requirements, notMetRequirements); } + if (this.notMetRequirements == notMetRequirements) { + return; + } + this.notMetRequirements = notMetRequirements; pendingMessages++; internalHandler .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) @@ -747,10 +762,6 @@ private void setDownloadsPaused(boolean downloadsPaused) { } private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { - // TODO: Move this deduplication check to the main thread. - if (this.notMetRequirements == notMetRequirements) { - return; - } this.notMetRequirements = notMetRequirements; logdFlags("Not met requirements are changed", notMetRequirements); for (int i = 0; i < downloadInternals.size(); i++) { 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 2306363cf55..00b08dc76a0 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 @@ -17,43 +17,43 @@ import java.io.IOException; -/** An writable index of {@link Download Downloads}. */ +/** A writable index of {@link Download Downloads}. */ public interface WritableDownloadIndex extends DownloadIndex { /** * Adds or replaces a {@link Download}. * * @param download The {@link Download} to be added. - * @throws throws IOException If an error occurs setting the state. + * @throws IOException If an error occurs setting the state. */ void putDownload(Download download) throws IOException; /** - * Removes the {@link Download} with the given {@code id}. + * Removes the download with the given ID. Does nothing if a download with the given ID does not + * exist. * - * @param id ID of a {@link Download}. - * @throws throws IOException If an error occurs removing the state. + * @param id The ID of the download to remove. + * @throws IOException If an error occurs removing the state. */ void removeDownload(String id) throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). * * @param stopReason The stop reason. - * @throws throws IOException If an error occurs updating the state. + * @throws IOException If an error occurs updating the state. */ void setStopReason(int stopReason) throws IOException; /** - * Sets the stop reason of the download with the given {@code id} in a terminal state ({@link - * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). - * - *

    If there's no {@link Download} with the given {@code id} or it isn't in a terminal state, - * then nothing happens. + * Sets the stop reason of the download with the given ID in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the + * given ID does not exist, or if it's not in a terminal state. * - * @param id ID of a {@link Download}. + * @param id The ID of the download to update. * @param stopReason The stop reason. - * @throws throws IOException If an error occurs updating the state. + * @throws IOException If an error occurs updating the state. */ void setStopReason(String id, int stopReason) throws IOException; } From 6c1065c6d25d682521d8bcdf0728f5712d5a3ab2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 30 Apr 2019 12:26:39 +0100 Subject: [PATCH 048/219] Prevent index out of bounds exceptions in some live HLS scenarios Can happen if the load position falls behind in every playlist and when we try to load the next segment, the adaptive selection logic decides to change variant. Issue:#5816 PiperOrigin-RevId: 245923006 --- RELEASENOTES.md | 2 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aac46647cc2..9e69bcc9173 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -103,6 +103,8 @@ ([#5441](https://github.com/google/ExoPlayer/issues/5441)). * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). * Support for playing spherical videos on Daydream. * Cast extension: Work around Cast framework returning a limited-size queue items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 92756f19cf5..261c9b531cd 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -278,8 +278,7 @@ public void getNextChunk( long chunkMediaSequence = getChunkMediaSequence( previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - if (previous != null && switchingTrack) { + if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. selectedTrackIndex = oldTrackIndex; @@ -289,10 +288,11 @@ public void getNextChunk( startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); chunkMediaSequence = previous.getNextChunkIndex(); - } else { - fatalError = new BehindLiveWindowException(); - return; - } + } + + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; } int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); From d215b81167f1b8768a2fb24ada4cfd72b2837bd1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 19:19:02 +0100 Subject: [PATCH 049/219] Rework DownloadManager to fix remaining TODOs - Removed DownloadInternal and its sometimes-out-of-sync duplicate state - Fixed downloads being in STOPPED rather than QUEUED state when the manager is paused - Fixed setMaxParallelDownloads to start/stop downloads if necessary when the value changes - Fixed isWaitingForRequirements PiperOrigin-RevId: 246164845 --- .../offline/ActionFileUpgradeUtil.java | 9 +- .../offline/DefaultDownloadIndex.java | 24 +- .../exoplayer2/offline/DownloadManager.java | 772 +++++++++--------- .../offline/WritableDownloadIndex.java | 7 + .../offline/ActionFileUpgradeUtilTest.java | 14 +- .../offline/DownloadManagerTest.java | 54 +- 6 files changed, 441 insertions(+), 439 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 975fc10b93f..baf47772ab5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -67,11 +67,12 @@ public static void upgradeAndDelete( if (actionFile.exists()) { boolean success = false; try { + long nowMs = System.currentTimeMillis(); for (DownloadRequest request : actionFile.load()) { if (downloadIdProvider != null) { request = request.copyWithId(downloadIdProvider.getId(request)); } - mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted); + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted, nowMs); } success = true; } finally { @@ -93,13 +94,13 @@ public static void upgradeAndDelete( /* package */ static void mergeRequest( DownloadRequest request, DefaultDownloadIndex downloadIndex, - boolean addNewDownloadAsCompleted) + boolean addNewDownloadAsCompleted, + long nowMs) throws IOException { Download download = downloadIndex.getDownload(request.id); if (download != null) { - download = DownloadManager.mergeRequest(download, request, download.stopReason); + download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); } else { - long nowMs = System.currentTimeMillis(); download = new Download( request, 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 252c058b88e..06f308d1e93 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 @@ -69,7 +69,9 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13; private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; - private static final String WHERE_STATE_TERMINAL = + private static final String WHERE_STATE_IS_DOWNLOADING = + COLUMN_STATE + " = " + Download.STATE_DOWNLOADING; + private static final String WHERE_STATE_IS_TERMINAL = getStateQuery(Download.STATE_COMPLETED, Download.STATE_FAILED); private static final String[] COLUMNS = @@ -218,6 +220,19 @@ public void removeDownload(String id) throws DatabaseIOException { } } + @Override + public void setDownloadingStatesToQueued() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_QUEUED); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, WHERE_STATE_IS_DOWNLOADING, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); @@ -225,7 +240,7 @@ public void setStopReason(int stopReason) throws DatabaseIOException { ContentValues values = new ContentValues(); values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.update(tableName, values, WHERE_STATE_TERMINAL, /* whereArgs= */ null); + writableDatabase.update(tableName, values, WHERE_STATE_IS_TERMINAL, /* whereArgs= */ null); } catch (SQLException e) { throw new DatabaseIOException(e); } @@ -239,7 +254,10 @@ public void setStopReason(String id, int stopReason) throws DatabaseIOException values.put(COLUMN_STOP_REASON, stopReason); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update( - tableName, values, WHERE_STATE_TERMINAL + " AND " + WHERE_ID_EQUALS, new String[] {id}); + tableName, + values, + WHERE_STATE_IS_TERMINAL + " AND " + WHERE_ID_EQUALS, + new String[] {id}); } catch (SQLException e) { throw new DatabaseIOException(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 8502a56ea73..b528d917594 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 @@ -31,7 +31,6 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -46,14 +45,11 @@ import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Manages downloads. @@ -125,8 +121,7 @@ default void onRequirementsStateChanged( // Messages posted to the main handler. private static final int MSG_INITIALIZED = 0; private static final int MSG_PROCESSED = 1; - private static final int MSG_DOWNLOAD_CHANGED = 2; - private static final int MSG_DOWNLOAD_REMOVED = 3; + private static final int MSG_DOWNLOAD_UPDATE = 2; // Messages posted to the background handler. private static final int MSG_INITIALIZE = 0; @@ -141,31 +136,14 @@ default void onRequirementsStateChanged( private static final int MSG_CONTENT_LENGTH_CHANGED = 9; private static final int MSG_RELEASE = 10; - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - START_THREAD_SUCCEEDED, - START_THREAD_WAIT_REMOVAL_TO_FINISH, - START_THREAD_WAIT_DOWNLOAD_CANCELLATION, - START_THREAD_TOO_MANY_DOWNLOADS - }) - private @interface StartThreadResults {} - - private static final int START_THREAD_SUCCEEDED = 0; - private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1; - private static final int START_THREAD_WAIT_DOWNLOAD_CANCELLATION = 2; - private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3; - private static final String TAG = "DownloadManager"; - private static final boolean DEBUG = false; private final Context context; private final WritableDownloadIndex downloadIndex; private final Handler mainHandler; private final InternalHandler internalHandler; private final RequirementsWatcher.Listener requirementsListener; - private final CopyOnWriteArraySet listeners; - private final ArrayList downloads; private int pendingMessages; private int activeTaskCount; @@ -174,6 +152,7 @@ default void onRequirementsStateChanged( private int maxParallelDownloads; private int minRetryCount; private int notMetRequirements; + private List downloads; private RequirementsWatcher requirementsWatcher; /** @@ -205,11 +184,13 @@ public DownloadManager( Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { this.context = context.getApplicationContext(); this.downloadIndex = downloadIndex; + maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; minRetryCount = DEFAULT_MIN_RETRY_COUNT; downloadsPaused = true; - downloads = new ArrayList<>(); + downloads = Collections.emptyList(); listeners = new CopyOnWriteArraySet<>(); + requirementsListener = this::onRequirementsStateChanged; requirementsWatcher = new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); @@ -253,8 +234,14 @@ public boolean isIdle() { * reason that the {@link #getRequirements() Requirements} are not met. */ public boolean isWaitingForRequirements() { - // TODO: Fix this to return the right thing. - return !downloads.isEmpty(); + if (!downloadsPaused && notMetRequirements != 0) { + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == STATE_QUEUED) { + return true; + } + } + } + return false; } /** @@ -362,7 +349,7 @@ public DownloadIndex getDownloadIndex() { * #getDownloadIndex()} instead. */ public List getCurrentDownloads() { - return Collections.unmodifiableList(new ArrayList<>(downloads)); + return downloads; } /** Returns whether downloads are currently paused. */ @@ -475,10 +462,10 @@ public void release() { } mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. + downloads = Collections.emptyList(); pendingMessages = 0; activeTaskCount = 0; initialized = false; - downloads.clear(); } } @@ -508,13 +495,9 @@ private boolean handleMainMessage(Message message) { List downloads = (List) message.obj; onInitialized(downloads); break; - case MSG_DOWNLOAD_CHANGED: - Download state = (Download) message.obj; - onDownloadChanged(state); - break; - case MSG_DOWNLOAD_REMOVED: - state = (Download) message.obj; - onDownloadRemoved(state); + case MSG_DOWNLOAD_UPDATE: + DownloadUpdate update = (DownloadUpdate) message.obj; + onDownloadUpdate(update); break; case MSG_PROCESSED: int processedMessageCount = message.arg1; @@ -529,32 +512,23 @@ private boolean handleMainMessage(Message message) { private void onInitialized(List downloads) { initialized = true; - this.downloads.addAll(downloads); + this.downloads = Collections.unmodifiableList(downloads); for (Listener listener : listeners) { listener.onInitialized(DownloadManager.this); } } - private void onDownloadChanged(Download download) { - int downloadIndex = getDownloadIndex(download.request.id); - if (download.isTerminalState()) { - if (downloadIndex != C.INDEX_UNSET) { - downloads.remove(downloadIndex); + private void onDownloadUpdate(DownloadUpdate update) { + downloads = Collections.unmodifiableList(update.downloads); + Download updatedDownload = update.download; + if (update.isRemove) { + for (Listener listener : listeners) { + listener.onDownloadRemoved(this, updatedDownload); } - } else if (downloadIndex != C.INDEX_UNSET) { - downloads.set(downloadIndex, download); } else { - downloads.add(download); - } - for (Listener listener : listeners) { - listener.onDownloadChanged(this, download); - } - } - - private void onDownloadRemoved(Download download) { - downloads.remove(getDownloadIndex(download.request.id)); - for (Listener listener : listeners) { - listener.onDownloadRemoved(this, download); + for (Listener listener : listeners) { + listener.onDownloadChanged(this, updatedDownload); + } } } @@ -568,18 +542,14 @@ private void onMessageProcessed(int processedMessageCount, int activeTaskCount) } } - private int getDownloadIndex(String id) { - for (int i = 0; i < downloads.size(); i++) { - if (downloads.get(i).request.id.equals(id)) { - return i; - } - } - return C.INDEX_UNSET; - } - /* package */ static Download mergeRequest( - Download download, DownloadRequest request, int stopReason) { + Download download, DownloadRequest request, int stopReason, long nowMs) { @Download.State int state = download.state; + // Treat the merge as creating a new download if we're currently removing the existing one, or + // if the existing download is in a terminal state. Else treat the merge as updating the + // existing download. + long startTimeMs = + state == STATE_REMOVING || download.isTerminalState() ? nowMs : download.startTimeMs; if (state == STATE_REMOVING || state == STATE_RESTARTING) { state = STATE_RESTARTING; } else if (stopReason != STOP_REASON_NONE) { @@ -587,8 +557,6 @@ private int getDownloadIndex(String id) { } else { state = STATE_QUEUED; } - long nowMs = System.currentTimeMillis(); - long startTimeMs = download.isTerminalState() ? nowMs : download.startTimeMs; return new Download( download.request.copyWithMergedRequest(request), state, @@ -599,40 +567,6 @@ private int getDownloadIndex(String id) { FAILURE_REASON_NONE); } - private static Download copyWithState(Download download, @Download.State int state) { - return new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - download.stopReason, - FAILURE_REASON_NONE, - download.progress); - } - - private static void logd(String message) { - if (DEBUG) { - Log.d(TAG, message); - } - } - - private static void logd(String message, DownloadInternal downloadInternal) { - logd(message, downloadInternal.download.request); - } - - private static void logd(String message, DownloadRequest request) { - if (DEBUG) { - logd(message + ": " + request); - } - } - - private static void logdFlags(String message, int flags) { - if (DEBUG) { - logd(message + ": " + Integer.toBinaryString(flags)); - } - } - private static final class InternalHandler extends Handler { public boolean released; @@ -641,15 +575,14 @@ private static final class InternalHandler extends Handler { private final WritableDownloadIndex downloadIndex; private final DownloaderFactory downloaderFactory; private final Handler mainHandler; - private final ArrayList downloadInternals; + private final ArrayList downloads; private final HashMap activeTasks; - // Mutable fields that are accessed on the internal thread. @Requirements.RequirementFlags private int notMetRequirements; private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; - private int parallelDownloads; + private int activeDownloadTaskCount; public InternalHandler( HandlerThread thread, @@ -667,7 +600,7 @@ public InternalHandler( this.maxParallelDownloads = maxParallelDownloads; this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; - downloadInternals = new ArrayList<>(); + downloads = new ArrayList<>(); activeTasks = new HashMap<>(); } @@ -732,70 +665,91 @@ public void handleMessage(Message message) { private void initialize(int notMetRequirements) { this.notMetRequirements = notMetRequirements; - ArrayList loadedStates = new ArrayList<>(); - try (DownloadCursor cursor = - downloadIndex.getDownloads( - STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING)) { + DownloadCursor cursor = null; + try { + downloadIndex.setDownloadingStatesToQueued(); + cursor = + downloadIndex.getDownloads( + STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING); while (cursor.moveToNext()) { - loadedStates.add(cursor.getDownload()); + downloads.add(cursor.getDownload()); } - logd("Downloads are loaded."); - } catch (Throwable e) { - Log.e(TAG, "Download state loading failed.", e); - loadedStates.clear(); - } - for (Download download : loadedStates) { - addDownloadForState(download); - } - logd("Downloads are created."); - mainHandler.obtainMessage(MSG_INITIALIZED, loadedStates).sendToTarget(); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).start(); + } catch (IOException e) { + Log.e(TAG, "Failed to load index.", e); + downloads.clear(); + } finally { + Util.closeQuietly(cursor); } + // A copy must be used for the message to ensure that subsequent changes to the downloads list + // are not visible to the main thread when it processes the message. + ArrayList downloadsForMessage = new ArrayList<>(downloads); + mainHandler.obtainMessage(MSG_INITIALIZED, downloadsForMessage).sendToTarget(); + syncTasks(); } private void setDownloadsPaused(boolean downloadsPaused) { this.downloadsPaused = downloadsPaused; - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } + syncTasks(); } private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { this.notMetRequirements = notMetRequirements; - logdFlags("Not met requirements are changed", notMetRequirements); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } + syncTasks(); } private void setStopReason(@Nullable String id, int stopReason) { - if (id != null) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - logd("download stop reason is set to : " + stopReason, downloadInternal); - downloadInternal.setStopReason(stopReason); - return; + if (id == null) { + for (int i = 0; i < downloads.size(); i++) { + setStopReason(downloads.get(i), stopReason); + } + try { + // Set the stop reason for downloads in terminal states as well. + downloadIndex.setStopReason(stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason", e); } } else { - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).setStopReason(stopReason); + Download download = getDownload(id, /* loadFromIndex= */ false); + if (download != null) { + setStopReason(download, stopReason); + } else { + try { + // Set the stop reason if the download is in a terminal state. + downloadIndex.setStopReason(id, stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason: " + id, e); + } } } - try { - if (id != null) { - downloadIndex.setStopReason(id, stopReason); - } else { - downloadIndex.setStopReason(stopReason); + syncTasks(); + } + + private void setStopReason(Download download, int stopReason) { + if (stopReason == STOP_REASON_NONE) { + if (download.state == STATE_STOPPED) { + putDownloadWithState(download, STATE_QUEUED); } - } catch (IOException e) { - Log.e(TAG, "setStopReason failed", e); + } else if (stopReason != download.stopReason) { + @Download.State int state = download.state; + if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { + state = STATE_STOPPED; + } + putDownload( + new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + stopReason, + FAILURE_REASON_NONE, + download.progress)); } } private void setMaxParallelDownloads(int maxParallelDownloads) { this.maxParallelDownloads = maxParallelDownloads; - // TODO: Start or stop downloads if necessary. + syncTasks(); } private void setMinRetryCount(int minRetryCount) { @@ -803,77 +757,44 @@ private void setMinRetryCount(int minRetryCount) { } private void addDownload(DownloadRequest request, int stopReason) { - DownloadInternal downloadInternal = getDownload(request.id); - if (downloadInternal != null) { - downloadInternal.addRequest(request, stopReason); - logd("Request is added to existing download", downloadInternal); + Download download = getDownload(request.id, /* loadFromIndex= */ true); + long nowMs = System.currentTimeMillis(); + if (download != null) { + putDownload(mergeRequest(download, request, stopReason, nowMs)); } else { - Download download = loadDownload(request.id); - if (download == null) { - long nowMs = System.currentTimeMillis(); - download = - new Download( - request, - stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs, - /* contentLength= */ C.LENGTH_UNSET, - stopReason, - Download.FAILURE_REASON_NONE); - logd("Download state is created for " + request.id); - } else { - download = mergeRequest(download, request, stopReason); - logd("Download state is loaded for " + request.id); - } - addDownloadForState(download); + putDownload( + new Download( + request, + stopReason != STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE)); } + syncTasks(); } private void removeDownload(String id) { - DownloadInternal downloadInternal = getDownload(id); - if (downloadInternal != null) { - downloadInternal.remove(); - } else { - Download download = loadDownload(id); - if (download != null) { - addDownloadForState(copyWithState(download, STATE_REMOVING)); - } else { - logd("Can't remove download. No download with id: " + id); - } - } - } - - private void onTaskStopped(Task task) { - logd("Task is stopped", task.request); - String downloadId = task.request.id; - activeTasks.remove(downloadId); - boolean tryToStartDownloads = false; - if (!task.isRemove) { - // If maxParallelDownloads was hit, there might be a download waiting for a slot. - tryToStartDownloads = parallelDownloads == maxParallelDownloads; - parallelDownloads--; - } - getDownload(downloadId).onTaskStopped(task.isCanceled, task.finalError); - if (tryToStartDownloads) { - for (int i = 0; - parallelDownloads < maxParallelDownloads && i < downloadInternals.size(); - i++) { - downloadInternals.get(i).start(); - } + Download download = getDownload(id, /* loadFromIndex= */ true); + if (download == null) { + Log.e(TAG, "Failed to remove nonexistent download: " + id); + return; } - } - - private void onContentLengthChanged(Task task) { - String downloadId = task.request.id; - getDownload(downloadId).setContentLength(task.contentLength); + putDownloadWithState(download, STATE_REMOVING); + syncTasks(); } private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); } - activeTasks.clear(); - downloadInternals.clear(); + try { + downloadIndex.setDownloadingStatesToQueued(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + downloads.clear(); thread.quit(); synchronized (this) { released = true; @@ -881,261 +802,293 @@ private void release() { } } - private void onDownloadChanged(DownloadInternal downloadInternal, Download download) { - logd("Download state is changed", downloadInternal); - try { - downloadIndex.putDownload(download); - } catch (IOException e) { - Log.e(TAG, "Failed to update index", e); - } - if (downloadInternal.state == STATE_COMPLETED || downloadInternal.state == STATE_FAILED) { - downloadInternals.remove(downloadInternal); + // Start and cancel tasks based on the current download and manager states. + + private void syncTasks() { + int accumulatingDownloadTaskCount = 0; + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + Task activeTask = activeTasks.get(download.request.id); + switch (download.state) { + case STATE_STOPPED: + syncStoppedDownload(activeTask); + break; + case STATE_QUEUED: + activeTask = syncQueuedDownload(activeTask, download); + break; + case STATE_DOWNLOADING: + activeTask = Assertions.checkNotNull(activeTask); + syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + syncRemovingDownload(activeTask, download); + break; + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } + if (activeTask != null && !activeTask.isRemove) { + accumulatingDownloadTaskCount++; + } } - mainHandler.obtainMessage(MSG_DOWNLOAD_CHANGED, download).sendToTarget(); } - private void onDownloadRemoved(DownloadInternal downloadInternal, Download download) { - logd("Download is removed", downloadInternal); - try { - downloadIndex.removeDownload(download.request.id); - } catch (IOException e) { - Log.e(TAG, "Failed to remove from index", e); + private void syncStoppedDownload(@Nullable Task activeTask) { + if (activeTask != null) { + // We have a task, which must be a download task. Cancel it. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); } - downloadInternals.remove(downloadInternal); - mainHandler.obtainMessage(MSG_DOWNLOAD_REMOVED, download).sendToTarget(); } - @StartThreadResults - private int startTask(DownloadInternal downloadInternal) { - DownloadRequest request = downloadInternal.download.request; - String downloadId = request.id; - if (activeTasks.containsKey(downloadId)) { - if (stopDownloadTask(downloadId)) { - return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; - } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; + private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + // We have a task, which must be a download task. If the download state is queued we need to + // cancel it and start a new one, since a new request has been merged into the download. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); + return activeTask; } - boolean isRemove = downloadInternal.isInRemoveState(); - if (!isRemove) { - if (parallelDownloads == maxParallelDownloads) { - return START_THREAD_TOO_MANY_DOWNLOADS; - } - parallelDownloads++; + + if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) { + return null; } - Downloader downloader = downloaderFactory.createDownloader(request); - DownloadProgress downloadProgress = downloadInternal.download.progress; - Task task = + + // We can start a download task. + download = putDownloadWithState(download, STATE_DOWNLOADING); + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = new Task( - request, + download.request, downloader, - downloadProgress, - isRemove, + download.progress, + /* isRemove= */ false, minRetryCount, /* internalHandler= */ this); - activeTasks.put(downloadId, task); - task.start(); - logd("Task is started", downloadInternal); - return START_THREAD_SUCCEEDED; + activeTasks.put(download.request.id, activeTask); + activeDownloadTaskCount++; + activeTask.start(); + return activeTask; } - private boolean stopDownloadTask(String downloadId) { - Task task = activeTasks.get(downloadId); - if (task != null && !task.isRemove) { - task.cancel(/* released= */ false); - logd("Task is cancelled", task.request); - return true; + private void syncDownloadingDownload( + Task activeTask, Download download, int accumulatingDownloadTaskCount) { + Assertions.checkState(!activeTask.isRemove); + if (!canDownloadsRun() || accumulatingDownloadTaskCount >= maxParallelDownloads) { + putDownloadWithState(download, STATE_QUEUED); + activeTask.cancel(/* released= */ false); } - return false; } - @Nullable - private DownloadInternal getDownload(String id) { - for (int i = 0; i < downloadInternals.size(); i++) { - DownloadInternal downloadInternal = downloadInternals.get(i); - if (downloadInternal.download.request.id.equals(id)) { - return downloadInternal; + private void syncRemovingDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + if (!activeTask.isRemove) { + // Cancel the downloading task. + activeTask.cancel(/* released= */ false); } + // The activeTask is either a remove task, or a downloading task that we just cancelled. In + // the latter case we need to wait for the task to stop before we start a remove task. + return; } - return null; - } - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); - } - return null; + // We can start a remove task. + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ true, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + activeTask.start(); } - private void addDownloadForState(Download download) { - DownloadInternal downloadInternal = new DownloadInternal(this, download); - downloadInternals.add(downloadInternal); - logd("Download is added", downloadInternal); - downloadInternal.initialize(); - } + // Task event processing. - private boolean canStartDownloads() { - return !downloadsPaused && notMetRequirements == 0; + private void onContentLengthChanged(Task task) { + String downloadId = task.request.id; + long contentLength = task.contentLength; + Download download = getDownload(downloadId, /* loadFromIndex= */ false); + if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { + return; + } + putDownload( + new Download( + download.request, + download.state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + contentLength, + download.stopReason, + download.failureReason, + download.progress)); } - } - - private static final class DownloadInternal { - private final InternalHandler internalHandler; - - private Download download; + private void onTaskStopped(Task task) { + String downloadId = task.request.id; + activeTasks.remove(downloadId); - // TODO: Get rid of these and use download directly. - @Download.State private int state; - private long contentLength; - private int stopReason; - @MonotonicNonNull @Download.FailureReason private int failureReason; + boolean isRemove = task.isRemove; + if (!isRemove) { + activeDownloadTaskCount--; + } - private DownloadInternal(InternalHandler internalHandler, Download download) { - this.internalHandler = internalHandler; - this.download = download; - state = download.state; - contentLength = download.contentLength; - stopReason = download.stopReason; - failureReason = download.failureReason; - } + if (task.isCanceled) { + syncTasks(); + return; + } - private void initialize() { - initialize(download.state); - } + Throwable finalError = task.finalError; + if (finalError != null) { + Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); + } - public void addRequest(DownloadRequest newRequest, int stopReason) { - download = mergeRequest(download, newRequest, stopReason); - initialize(); - } + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); + switch (download.state) { + case STATE_DOWNLOADING: + Assertions.checkState(!isRemove); + onDownloadTaskStopped(download, finalError); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + Assertions.checkState(isRemove); + onRemoveTaskStopped(download); + break; + case STATE_QUEUED: + case STATE_STOPPED: + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } - public void remove() { - initialize(STATE_REMOVING); + syncTasks(); } - public Download getUpdatedDownload() { + private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) { download = new Download( download.request, - state, + finalError == null ? STATE_COMPLETED : STATE_FAILED, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - contentLength, - stopReason, - state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, + download.contentLength, + download.stopReason, + finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN, download.progress); - return download; - } - - public boolean isIdle() { - return state != STATE_DOWNLOADING && state != STATE_REMOVING && state != STATE_RESTARTING; - } - - @Override - public String toString() { - return download.request.id + ' ' + Download.getStateString(state); + // The download is now in a terminal state, so should not be in the downloads list. + downloads.remove(getDownloadIndex(download.request.id)); + // We still need to update the download index and main thread. + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); } - public void start() { - if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { - startOrQueue(); - } else if (isInRemoveState()) { - internalHandler.startTask(this); + private void onRemoveTaskStopped(Download download) { + if (download.state == STATE_RESTARTING) { + putDownloadWithState( + download, download.stopReason == STOP_REASON_NONE ? STATE_QUEUED : STATE_STOPPED); + syncTasks(); + } else { + int removeIndex = getDownloadIndex(download.request.id); + downloads.remove(removeIndex); + try { + downloadIndex.removeDownload(download.request.id); + } catch (IOException e) { + Log.e(TAG, "Failed to remove from database"); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); } } - public void setStopReason(int stopReason) { - this.stopReason = stopReason; - updateStopState(); - } + // Helper methods. - public boolean isInRemoveState() { - return state == STATE_REMOVING || state == STATE_RESTARTING; + private boolean canDownloadsRun() { + return !downloadsPaused && notMetRequirements == 0; } - public void setContentLength(long contentLength) { - if (this.contentLength == contentLength) { - return; - } - this.contentLength = contentLength; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); + private Download putDownloadWithState(Download download, @Download.State int state) { + // Downloads in terminal states shouldn't be in the downloads list. This method cannot be used + // 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)); } - private void updateStopState() { - Download oldDownload = download; - if (canStart()) { - if (state == STATE_STOPPED) { - startOrQueue(); - } + private Download putDownload(Download download) { + // Downloads in terminal states shouldn't be in the downloads list. + Assertions.checkState(download.state != STATE_COMPLETED && download.state != STATE_FAILED); + int changedIndex = getDownloadIndex(download.request.id); + if (changedIndex == C.INDEX_UNSET) { + downloads.add(download); + Collections.sort(downloads, InternalHandler::compareStartTimes); } else { - if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - internalHandler.stopDownloadTask(download.request.id); - setState(STATE_STOPPED); + boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs; + downloads.set(changedIndex, download); + if (needsSort) { + Collections.sort(downloads, InternalHandler::compareStartTimes); } } - if (oldDownload == download) { - internalHandler.onDownloadChanged(this, getUpdatedDownload()); + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + return download; } - private void initialize(int initialState) { - // Don't notify listeners with initial state until we make sure we don't switch to another - // state immediately. - state = initialState; - if (isInRemoveState()) { - internalHandler.startTask(this); - } else if (canStart()) { - startOrQueue(); - } else { - setState(STATE_STOPPED); - } - if (state == initialState) { - internalHandler.onDownloadChanged(this, getUpdatedDownload()); + @Nullable + private Download getDownload(String id, boolean loadFromIndex) { + int index = getDownloadIndex(id); + if (index != C.INDEX_UNSET) { + return downloads.get(index); } - } - - private boolean canStart() { - return internalHandler.canStartDownloads() && stopReason == STOP_REASON_NONE; - } - - private void startOrQueue() { - Assertions.checkState(!isInRemoveState()); - @StartThreadResults int result = internalHandler.startTask(this); - Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); - if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { - setState(STATE_DOWNLOADING); - } else { - setState(STATE_QUEUED); + if (loadFromIndex) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "Failed to load download: " + id, e); + } } + return null; } - private void setState(@Download.State int newState) { - if (state != newState) { - state = newState; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); + private int getDownloadIndex(String id) { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.request.id.equals(id)) { + return i; + } } + return C.INDEX_UNSET; } - private void onTaskStopped(boolean isCanceled, @Nullable Throwable error) { - if (isIdle()) { - return; - } - if (isCanceled) { - internalHandler.startTask(this); - } else if (state == STATE_REMOVING) { - internalHandler.onDownloadRemoved(this, getUpdatedDownload()); - } else if (state == STATE_RESTARTING) { - initialize(STATE_QUEUED); - } else { // STATE_DOWNLOADING - if (error != null) { - Log.e(TAG, "Download failed: " + download.request.id, error); - failureReason = FAILURE_REASON_UNKNOWN; - setState(STATE_FAILED); - } else { - setState(STATE_COMPLETED); - } - } + private static int compareStartTimes(Download first, Download second) { + return Util.compareLong(first.startTimeMs, second.startTimeMs); } } @@ -1177,16 +1130,17 @@ public void cancel(boolean released) { // download manager whilst cancellation is ongoing. internalHandler = null; } - isCanceled = true; - downloader.cancel(); - interrupt(); + if (!isCanceled) { + isCanceled = true; + downloader.cancel(); + interrupt(); + } } // Methods running on download thread. @Override public void run() { - logd("Download started", request); try { if (isRemove) { downloader.remove(); @@ -1201,14 +1155,12 @@ public void run() { if (!isCanceled) { long bytesDownloaded = downloadProgress.bytesDownloaded; if (bytesDownloaded != errorPosition) { - logd("Reset error count. bytesDownloaded = " + bytesDownloaded, request); errorPosition = bytesDownloaded; errorCount = 0; } if (++errorCount > minRetryCount) { throw e; } - logd("Download error. Retry " + errorCount, request); Thread.sleep(getRetryDelayMillis(errorCount)); } } @@ -1240,4 +1192,18 @@ private static int getRetryDelayMillis(int errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } } + + private static final class DownloadUpdate { + + private final Download download; + private final boolean isRemove; + + private final List downloads; + + public DownloadUpdate(Download download, boolean isRemove, List downloads) { + this.download = download; + this.isRemove = isRemove; + this.downloads = downloads; + } + } } 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 00b08dc76a0..ae634f8544b 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 @@ -37,6 +37,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void removeDownload(String id) throws IOException; + /** + * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}. + * + * @throws IOException If an error occurs updating the state. + */ + void setDownloadingStatesToQueued() 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/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java index dba7b74e9f5..2f36b7f48cb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.java @@ -38,6 +38,8 @@ @RunWith(AndroidJUnit4.class) public class ActionFileUpgradeUtilTest { + private static final long NOW_MS = 1234; + private File tempFile; private ExoDatabaseProvider databaseProvider; private DefaultDownloadIndex downloadIndex; @@ -113,7 +115,7 @@ public void mergeRequest_nonExistingDownload_createsNewDownload() throws IOExcep data); ActionFileUpgradeUtil.mergeRequest( - request, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -141,9 +143,9 @@ public void mergeRequest_existingDownload_createsMergedDownload() throws IOExcep /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); ActionFileUpgradeUtil.mergeRequest( - request2, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request2, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); Download download = downloadIndex.getDownload(request2.id); assertThat(download).isNotNull(); @@ -178,16 +180,16 @@ public void mergeRequest_addNewDownloadAsCompleted() throws IOException { /* customCacheKey= */ "key123", new byte[] {5, 4, 3, 2, 1}); ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); // Merging existing download, keeps it queued. ActionFileUpgradeUtil.mergeRequest( - request1, downloadIndex, /* addNewDownloadAsCompleted= */ true); + request1, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS); assertThat(downloadIndex.getDownload(request1.id).state).isEqualTo(Download.STATE_QUEUED); // New download is merged as completed. ActionFileUpgradeUtil.mergeRequest( - request2, downloadIndex, /* addNewDownloadAsCompleted= */ true); + request2, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS); assertThat(downloadIndex.getDownload(request2.id).state).isEqualTo(Download.STATE_COMPLETED); } 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 92c6debdd8d..2b9ef11235a 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 @@ -61,6 +61,8 @@ public class DownloadManagerTest { private static final int APP_STOP_REASON = 1; /** The minimum number of times a task must be retried before failing. */ private static final int MIN_RETRY_COUNT = 3; + /** Dummy value for the current time. */ + private static final long NOW_MS = 1234; private Uri uri1; private Uri uri2; @@ -132,6 +134,7 @@ public void postDownloadRequest_downloads() throws Throwable { task.assertCompleted(); runner.assertCreatedDownloaderCount(1); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -143,6 +146,7 @@ public void postRemoveRequest_removes() throws Throwable { task.assertRemoved(); runner.assertCreatedDownloaderCount(2); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -158,6 +162,7 @@ public void downloadFails_retriesThenTaskFails() throws Throwable { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertFailed(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -174,6 +179,7 @@ public void downloadFails_retries() throws Throwable { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertCompleted(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -341,7 +347,7 @@ public void downloadRequestFollowingRemove_ifMaxDownloadIs1_isNotStarted() throw } @Test - public void getTasks_returnTasks() { + public void getCurrentDownloads_returnsCurrentDownloads() { TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask(); TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask(); TaskWrapper task3 = @@ -370,13 +376,11 @@ public void pauseAndResume() throws Throwable { runOnMainThread(() -> downloadManager.pauseDownloads()); - // TODO: This should be assertQueued. Fix implementation and update test. - runner1.getTask().assertStopped(); + runner1.getTask().assertQueued(); // remove requests aren't stopped. runner2.getDownloader(1).unblock().assertReleased(); - // TODO: This should be assertQueued. Fix implementation and update test. - runner2.getTask().assertStopped(); + runner2.getTask().assertQueued(); // Although remove2 is finished, download2 doesn't start. runner2.getDownloader(2).assertDoesNotStart(); @@ -397,7 +401,7 @@ public void pauseAndResume() throws Throwable { } @Test - public void manuallyStopAndResumeSingleDownload() throws Throwable { + public void setAndClearSingleDownloadStopReason() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); TaskWrapper task = runner.getTask(); @@ -415,7 +419,7 @@ public void manuallyStopAndResumeSingleDownload() throws Throwable { } @Test - public void manuallyStoppedDownloadCanBeCancelled() throws Throwable { + public void setSingleDownloadStopReasonThenRemove_removesDownload() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); TaskWrapper task = runner.getTask(); @@ -433,7 +437,7 @@ public void manuallyStoppedDownloadCanBeCancelled() throws Throwable { } @Test - public void manuallyStoppedSingleDownload_doesNotAffectOthers() throws Throwable { + public void setSingleDownloadStopReason_doesNotAffectOtherDownloads() throws Throwable { DownloadRunner runner1 = new DownloadRunner(uri1); DownloadRunner runner2 = new DownloadRunner(uri2); DownloadRunner runner3 = new DownloadRunner(uri3); @@ -455,21 +459,22 @@ public void manuallyStoppedSingleDownload_doesNotAffectOthers() throws Throwable } @Test - public void mergeRequest_removingDownload_becomesRestarting() { + public void mergeRequest_removing_becomesRestarting() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest).setState(Download.STATE_REMOVING); Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - Download expectedDownload = downloadBuilder.setState(Download.STATE_RESTARTING).build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + Download expectedDownload = + downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_RESTARTING).build(); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } @Test - public void mergeRequest_failedDownload_becomesQueued() { + public void mergeRequest_failed_becomesQueued() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -478,18 +483,19 @@ public void mergeRequest_failedDownload_becomesQueued() { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); Download expectedDownload = downloadBuilder + .setStartTimeMs(NOW_MS) .setState(Download.STATE_QUEUED) .setFailureReason(Download.FAILURE_REASON_NONE) .build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } @Test - public void mergeRequest_stoppedDownload_staysStopped() { + public void mergeRequest_stopped_staysStopped() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -498,13 +504,13 @@ public void mergeRequest_stoppedDownload_staysStopped() { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - assertEqualIgnoringTimeFields(mergedDownload, download); + assertEqualIgnoringUpdateTime(mergedDownload, download); } @Test - public void mergeRequest_stopReasonSetButNotStopped_becomesStopped() { + public void mergeRequest_completedWithStopReason_becomesStopped() { DownloadRequest downloadRequest = createDownloadRequest(); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) @@ -513,10 +519,11 @@ public void mergeRequest_stopReasonSetButNotStopped_becomesStopped() { Download download = downloadBuilder.build(); Download mergedDownload = - DownloadManager.mergeRequest(download, downloadRequest, download.stopReason); + DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS); - Download expectedDownload = downloadBuilder.setState(Download.STATE_STOPPED).build(); - assertEqualIgnoringTimeFields(mergedDownload, expectedDownload); + Download expectedDownload = + downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_STOPPED).build(); + assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } private void setUpDownloadManager(final int maxParallelDownloads) throws Exception { @@ -554,9 +561,10 @@ private void runOnMainThread(TestRunnable r) { dummyMainThread.runTestOnMainThread(r); } - private static void assertEqualIgnoringTimeFields(Download download, Download that) { + private static void assertEqualIgnoringUpdateTime(Download download, Download that) { assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); + assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.failureReason).isEqualTo(that.failureReason); assertThat(download.stopReason).isEqualTo(that.stopReason); From 214a372e062f9740644d73501c0a08e338ac5657 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 20:06:11 +0100 Subject: [PATCH 050/219] Periodically persist progress to index whilst downloading PiperOrigin-RevId: 246173972 --- .../exoplayer2/offline/DownloadManager.java | 37 +++++++++++++++---- .../exoplayer2/offline/DownloadService.java | 4 +- 2 files changed, 32 insertions(+), 9 deletions(-) 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 b528d917594..3e0375718bb 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 @@ -134,7 +134,8 @@ default void onRequirementsStateChanged( 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_RELEASE = 10; + private static final int MSG_UPDATE_PROGRESS = 10; + private static final int MSG_RELEASE = 11; private static final String TAG = "DownloadManager"; @@ -569,6 +570,8 @@ private void onMessageProcessed(int processedMessageCount, int activeTaskCount) private static final class InternalHandler extends Handler { + private static final int UPDATE_PROGRESS_INTERVAL_MS = 5000; + public boolean released; private final HandlerThread thread; @@ -650,11 +653,13 @@ public void handleMessage(Message message) { case MSG_CONTENT_LENGTH_CHANGED: task = (Task) message.obj; onContentLengthChanged(task); - processedExternalMessage = false; // This message is posted internally. - break; + return; // No need to post back to mainHandler. + case MSG_UPDATE_PROGRESS: + updateProgress(); + return; // No need to post back to mainHandler. case MSG_RELEASE: release(); - return; // Don't post back to mainHandler on release. + return; // No need to post back to mainHandler. default: throw new IllegalStateException(); } @@ -868,7 +873,9 @@ private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { minRetryCount, /* internalHandler= */ this); activeTasks.put(download.request.id, activeTask); - activeDownloadTaskCount++; + if (activeDownloadTaskCount++ == 0) { + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } activeTask.start(); return activeTask; } @@ -933,8 +940,8 @@ private void onTaskStopped(Task task) { activeTasks.remove(downloadId); boolean isRemove = task.isRemove; - if (!isRemove) { - activeDownloadTaskCount--; + if (!isRemove && --activeDownloadTaskCount == 0) { + removeMessages(MSG_UPDATE_PROGRESS); } if (task.isCanceled) { @@ -1013,6 +1020,22 @@ private void onRemoveTaskStopped(Download download) { } } + // Progress updates. + + private void updateProgress() { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.state == STATE_DOWNLOADING) { + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + } + } + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } + // Helper methods. private boolean canDownloadsRun() { 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 ee00cf3d5fc..ce9087c6c81 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 @@ -683,7 +683,7 @@ protected void onDownloadRemoved(Download download) { // Do nothing. } - private void notifyDownloadChange(Download download) { + private void notifyDownloadChanged(Download download) { onDownloadChanged(download); if (foregroundNotificationUpdater != null) { if (download.state == Download.STATE_DOWNLOADING @@ -834,7 +834,7 @@ public void detachService(DownloadService downloadService, boolean unschedule) { @Override public void onDownloadChanged(DownloadManager downloadManager, Download download) { if (downloadService != null) { - downloadService.notifyDownloadChange(download); + downloadService.notifyDownloadChanged(download); } } From 9f9cf316bd3c7740c3fa1fa5ccbfa0d5dd3c417b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 May 2019 20:50:44 +0100 Subject: [PATCH 051/219] Remove unnecessary logging As justification for why we should not have this type of logging, it would scale up to about 13K LOC, 1800 Strings, and 36K (after pro-guarding - in the case of the demo app) if we did it through the whole code base*. It makes the code messier to read, and in most cases doesn't add significant value. Note: I left the Scheduler logging because it logs interactions with some awkward library components outside of ExoPlayer, so is perhaps a bit more justified. * This is a bit unfair since realistically we wouldn't ever add lots of logging into trivial classes. But I think it is fair to say that the deltas would be non-negligible. PiperOrigin-RevId: 246181421 --- .../jobdispatcher/JobDispatcherScheduler.java | 1 + .../android/exoplayer2/offline/Download.java | 22 --------------- .../exoplayer2/offline/DownloadService.java | 27 +++++-------------- .../scheduler/PlatformScheduler.java | 1 + .../exoplayer2/scheduler/Requirements.java | 12 --------- .../scheduler/RequirementsWatcher.java | 23 ---------------- .../exoplayer2/scheduler/Scheduler.java | 2 -- .../testutil/TestDownloadManagerListener.java | 22 +-------------- 8 files changed, 10 insertions(+), 100 deletions(-) diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index 790f5ca4e59..d79dead0d76 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -57,6 +57,7 @@ */ public final class JobDispatcherScheduler implements Scheduler { + private static final boolean DEBUG = false; private static final String TAG = "JobDispatcherScheduler"; private static final String KEY_SERVICE_ACTION = "service_action"; private static final String KEY_SERVICE_PACKAGE = "service_package"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 00d81b392c4..97dff8394e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -81,28 +81,6 @@ public final class Download { /** The download isn't stopped. */ public static final int STOP_REASON_NONE = 0; - /** Returns the state string for the given state value. */ - public static String getStateString(@State int state) { - switch (state) { - case STATE_QUEUED: - return "QUEUED"; - case STATE_STOPPED: - return "STOPPED"; - case STATE_DOWNLOADING: - return "DOWNLOADING"; - case STATE_COMPLETED: - return "COMPLETED"; - case STATE_FAILED: - return "FAILED"; - case STATE_REMOVING: - return "REMOVING"; - case STATE_RESTARTING: - return "RESTARTING"; - default: - throw new IllegalStateException(); - } - } - /** The download request. */ public final DownloadRequest request; /** The state of the download. */ 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 ce9087c6c81..fdd7163a2c8 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 @@ -127,18 +127,18 @@ public abstract class DownloadService extends Service { public static final String KEY_DOWNLOAD_REQUEST = "download_request"; /** - * Key for the content id in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_REMOVE_DOWNLOAD} - * intents. + * Key for the {@link String} content id in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_REMOVE_DOWNLOAD} intents. */ public static final String KEY_CONTENT_ID = "content_id"; /** - * Key for the stop reason in {@link #ACTION_SET_STOP_REASON} and {@link #ACTION_ADD_DOWNLOAD} - * intents. + * Key for the integer stop reason in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_ADD_DOWNLOAD} intents. */ public static final String KEY_STOP_REASON = "stop_reason"; - /** Key for the requirements in {@link #ACTION_SET_REQUIREMENTS} intents. */ + /** Key for the {@link Requirements} in {@link #ACTION_SET_REQUIREMENTS} intents. */ public static final String KEY_REQUIREMENTS = "requirements"; /** @@ -155,7 +155,6 @@ public abstract class DownloadService extends Service { public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000; private static final String TAG = "DownloadService"; - private static final boolean DEBUG = false; // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the // process is running). This allows DownloadService to restart when there's no scheduler. @@ -506,7 +505,6 @@ public static void startForeground(Context context, Class { if (networkCallback != null) { - logd(RequirementsWatcher.this + " NetworkCallback"); checkRequirements(); } }); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java index 1b225d9a4d7..b5a6f404247 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -22,8 +22,6 @@ /** Schedules a service to be started in the foreground when some {@link Requirements} are met. */ public interface Scheduler { - /* package */ boolean DEBUG = false; - /** * Schedules a service to be started in the foreground when some {@link Requirements} are met. * Anything that was previously scheduled will be canceled. diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index 9d6223b8b11..4c334992b50 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -22,9 +22,7 @@ import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.offline.DownloadManager; -import java.util.ArrayList; import java.util.HashMap; -import java.util.Locale; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -138,7 +136,6 @@ public void assertState(String taskId, @State int expectedState, int timeoutMs) } private void assertStateInternal(String taskId, int expectedState, int timeoutMs) { - ArrayList receivedStates = new ArrayList<>(); while (true) { Integer state = null; try { @@ -150,25 +147,8 @@ private void assertStateInternal(String taskId, int expectedState, int timeoutMs if (expectedState == state) { return; } - receivedStates.add(state); } else { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < receivedStates.size(); i++) { - if (i > 0) { - sb.append(','); - } - int receivedState = receivedStates.get(i); - String receivedStateString = - receivedState == STATE_REMOVED ? "REMOVED" : Download.getStateString(receivedState); - sb.append(receivedStateString); - } - fail( - String.format( - Locale.US, - "for download (%s) expected:<%s> but was:<%s>", - taskId, - Download.getStateString(expectedState), - sb)); + fail("Didn't receive expected state: " + expectedState); } } } From 241ce2df490bc024814d33b08c26950f56c67920 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 2 May 2019 10:37:31 +0100 Subject: [PATCH 052/219] Post-submit fixes for https://github.com/google/ExoPlayer/commit/eed5d957d87d44cb9c716f1a4c80f39ad2a6a442. One wrong return value, a useless assignment, unusual visibility of private class fields and some nullability issues. PiperOrigin-RevId: 246282995 --- .../exoplayer2/offline/DownloadManager.java | 34 ++++++++++++------- .../google/android/exoplayer2/util/Util.java | 4 +-- 2 files changed, 23 insertions(+), 15 deletions(-) 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 3e0375718bb..e8b7eaf9b27 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 @@ -31,6 +31,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -192,12 +193,9 @@ public DownloadManager( downloads = Collections.emptyList(); listeners = new CopyOnWriteArraySet<>(); - requirementsListener = this::onRequirementsStateChanged; - requirementsWatcher = - new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); - notMetRequirements = requirementsWatcher.start(); - - mainHandler = new Handler(Util.getLooper(), this::handleMainMessage); + @SuppressWarnings("methodref.receiver.bound.invalid") + Handler mainHandler = Util.createHandler(this::handleMainMessage); + this.mainHandler = mainHandler; HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); internalThread.start(); internalHandler = @@ -210,6 +208,13 @@ public DownloadManager( minRetryCount, downloadsPaused); + @SuppressWarnings("methodref.receiver.bound.invalid") + RequirementsWatcher.Listener requirementsListener = this::onRequirementsStateChanged; + this.requirementsListener = requirementsListener; + requirementsWatcher = + new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); + notMetRequirements = requirementsWatcher.start(); + pendingMessages = 1; internalHandler .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0) @@ -822,7 +827,7 @@ private void syncTasks() { activeTask = syncQueuedDownload(activeTask, download); break; case STATE_DOWNLOADING: - activeTask = Assertions.checkNotNull(activeTask); + Assertions.checkNotNull(activeTask); syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount); break; case STATE_REMOVING: @@ -848,6 +853,8 @@ private void syncStoppedDownload(@Nullable Task activeTask) { } } + @Nullable + @CheckResult private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { if (activeTask != null) { // We have a task, which must be a download task. If the download state is queued we need to @@ -919,7 +926,8 @@ private void syncRemovingDownload(@Nullable Task activeTask, Download download) private void onContentLengthChanged(Task task) { String downloadId = task.request.id; long contentLength = task.contentLength; - Download download = getDownload(downloadId, /* loadFromIndex= */ false); + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { return; } @@ -1125,7 +1133,7 @@ private static class Task extends Thread implements Downloader.ProgressListener private volatile InternalHandler internalHandler; private volatile boolean isCanceled; - private Throwable finalError; + @Nullable private Throwable finalError; private long contentLength; @@ -1145,6 +1153,7 @@ private Task( contentLength = C.LENGTH_UNSET; } + @SuppressWarnings("nullness:assignment.type.incompatible") public void cancel(boolean released) { if (released) { // Download threads are GC roots for as long as they're running. The time taken for @@ -1218,10 +1227,9 @@ private static int getRetryDelayMillis(int errorCount) { private static final class DownloadUpdate { - private final Download download; - private final boolean isRemove; - - private final List downloads; + public final Download download; + public final boolean isRemove; + public final List downloads; public DownloadUpdate(Download download, boolean isRemove, List downloads) { this.download = download; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index c05486bedfd..97bcb687080 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -390,7 +390,7 @@ public static ExecutorService newSingleThreadExecutor(final String threadName) { * * @param dataSource The {@link DataSource} to close. */ - public static void closeQuietly(DataSource dataSource) { + public static void closeQuietly(@Nullable DataSource dataSource) { try { if (dataSource != null) { dataSource.close(); @@ -406,7 +406,7 @@ public static void closeQuietly(DataSource dataSource) { * * @param closeable The {@link Closeable} to close. */ - public static void closeQuietly(Closeable closeable) { + public static void closeQuietly(@Nullable Closeable closeable) { try { if (closeable != null) { closeable.close(); From c33835b4785f4253133c0951e46847894bfa39ba Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 2 May 2019 11:41:06 +0100 Subject: [PATCH 053/219] Fix SmoothStreaming links NOTE: Streams are working on ExoPlayer but querying them from other platforms yields "bad request". The new links: + Match Microsoft's test server. + Allow querying from clients other than ExoPlayer, like curl. PiperOrigin-RevId: 246289755 --- demos/main/src/main/assets/media.exolist.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index c2acf3990b2..bcb3ef4ad1a 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -330,11 +330,11 @@ "samples": [ { "name": "Super speed", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" }, { "name": "Super speed (PlayReady)", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", "drm_scheme": "playready" } ] From 116602d8c04ec977dbc364841ae0e1e882c3d155 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 May 2019 17:35:30 +0100 Subject: [PATCH 054/219] Minor download documentation tweaks PiperOrigin-RevId: 246333281 --- .../android/exoplayer2/offline/DefaultDownloaderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java index ca20c769dcf..d8126d47361 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -112,7 +112,7 @@ private static Constructor getDownloaderConstructor(Class< .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class); } catch (NoSuchMethodException e) { // The downloader is present, but the expected constructor is missing. - throw new RuntimeException("DASH downloader constructor missing", e); + throw new RuntimeException("Downloader constructor missing", e); } } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) From 71d7e0afe20e02fd83063bba024907c3da84be30 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 May 2019 13:14:38 +0100 Subject: [PATCH 055/219] Add a couple of assertions to DownloadManager set methods PiperOrigin-RevId: 246491511 --- .../google/android/exoplayer2/offline/DownloadManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e8b7eaf9b27..3bf03dd3e83 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 @@ -306,9 +306,10 @@ public int getMaxParallelDownloads() { /** * Sets the maximum number of parallel downloads. * - * @param maxParallelDownloads The maximum number of parallel downloads. + * @param maxParallelDownloads The maximum number of parallel downloads. Must be greater than 0. */ public void setMaxParallelDownloads(int maxParallelDownloads) { + Assertions.checkArgument(maxParallelDownloads > 0); if (this.maxParallelDownloads == maxParallelDownloads) { return; } @@ -334,6 +335,7 @@ public int getMinRetryCount() { * @param minRetryCount The minimum number of times that a download will be retried. */ public void setMinRetryCount(int minRetryCount) { + Assertions.checkArgument(minRetryCount >= 0); if (this.minRetryCount == minRetryCount) { return; } From ce37c799687315a8d488e2f524cc3321c71823ee Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 3 May 2019 21:12:02 +0100 Subject: [PATCH 056/219] Fix Javadoc --- .../android/exoplayer2/drm/OfflineLicenseHelper.java | 4 ++-- .../java/com/google/android/exoplayer2/util/GlUtil.java | 4 ++-- .../android/exoplayer2/ui/spherical/CanvasRenderer.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index ed77f41c83b..55a7a901ac7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -92,7 +92,7 @@ public static OfflineLicenseHelper newWidevineInstance( * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) + * MediaDrmCallback, HashMap) */ public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, @@ -115,7 +115,7 @@ public static OfflineLicenseHelper newWidevineInstance( * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) + * MediaDrmCallback, HashMap) */ public OfflineLicenseHelper( UUID uuid, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 915e855d233..7fc46dc363b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -51,7 +51,7 @@ public static void checkGlError() { } /** - * Builds a GL shader program from vertex & fragment shader code. + * Builds a GL shader program from vertex and fragment shader code. * * @param vertexCode GLES20 vertex shader program as arrays of strings. Strings are joined by * adding a new line character in between each of them. @@ -64,7 +64,7 @@ public static int compileProgram(String[] vertexCode, String[] fragmentCode) { } /** - * Builds a GL shader program from vertex & fragment shader code. + * Builds a GL shader program from vertex and fragment shader code. * * @param vertexCode GLES20 vertex shader program. * @param fragmentCode GLES20 fragment shader program. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java index fdd59101e7a..3d7e57bbd24 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java @@ -72,7 +72,7 @@ public final class CanvasRenderer { "}" }; - // The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position & 2 texture + // The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position and 2 texture // coordinates. private static final int POSITION_COORDS_PER_VERTEX = 3; private static final int TEXTURE_COORDS_PER_VERTEX = 2; @@ -253,8 +253,8 @@ public void shutdown() { * Translates an orientation into pixel coordinates on the canvas. * *

    This is a minimal hit detection system that works for this quad because it has no model - * matrix. All the math is based on the fact that its size & distance are hard-coded into this - * class. For a more complex 3D mesh, a general bounding box & ray collision system would be + * matrix. All the math is based on the fact that its size and distance are hard-coded into this + * class. For a more complex 3D mesh, a general bounding box and ray collision system would be * required. * * @param yaw Yaw of the orientation in radians. @@ -287,7 +287,7 @@ public PointF translateClick(float yaw, float pitch) { return null; } // Convert from the polar coordinates of the controller to the rectangular coordinates of the - // View. Note the negative yaw & pitch used to generate Android-compliant x & y coordinates. + // View. Note the negative yaw and pitch used to generate Android-compliant x and y coordinates. float clickXPixel = (float) (widthPixel - clickXUnit * widthPixel / widthUnit); float clickYPixel = (float) (heightPixel - clickYUnit * heightPixel / heightUnit); return new PointF(clickXPixel, clickYPixel); From 8b2d436d7c5483272cf034eca87eadaaecb6c206 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 May 2019 17:25:48 +0100 Subject: [PATCH 057/219] Prevent CachedContentIndex.idToKey from growing without bound PiperOrigin-RevId: 246727723 --- .../upstream/cache/CachedContentIndex.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 20a80a1a359..bc5443f3655 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -89,6 +89,8 @@ * efficiently when the index is next stored. */ private final SparseBooleanArray removedIds; + /** Tracks ids that are new since the index was last stored. */ + private final SparseBooleanArray newIds; private Storage storage; @Nullable private Storage previousStorage; @@ -150,6 +152,7 @@ public CachedContentIndex( keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); + newIds = new SparseBooleanArray(); Storage databaseStorage = databaseProvider != null ? new DatabaseStorage(databaseProvider) : null; Storage legacyStorage = @@ -206,6 +209,7 @@ public void store() throws IOException { idToKey.remove(removedIds.keyAt(i)); } removedIds.clear(); + newIds.clear(); } /** @@ -250,11 +254,19 @@ public void maybeRemove(String key) { CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); - storage.onRemove(cachedContent); - // Keep an entry in idToKey to stop the id from being reused until the index is next stored. - idToKey.put(cachedContent.id, /* value= */ null); - // Track that the entry should be removed from idToKey when the index is next stored. - removedIds.put(cachedContent.id, /* value= */ true); + int id = cachedContent.id; + boolean neverStored = newIds.get(id); + storage.onRemove(cachedContent, neverStored); + if (neverStored) { + // The id can be reused immediately. + idToKey.remove(id); + newIds.delete(id); + } else { + // Keep an entry in idToKey to stop the id from being reused until the index is next stored, + // and add an entry to removedIds to track that it should be removed when this does happen. + idToKey.put(id, /* value= */ null); + removedIds.put(id, /* value= */ true); + } } } @@ -297,8 +309,9 @@ public ContentMetadata getContentMetadata(String key) { private CachedContent addNew(String key) { int id = getNewId(idToKey); CachedContent cachedContent = new CachedContent(id, key); - keyToContent.put(cachedContent.key, cachedContent); - idToKey.put(cachedContent.id, cachedContent.key); + keyToContent.put(key, cachedContent); + idToKey.put(id, key); + newIds.put(id, true); storage.onUpdate(cachedContent); return cachedContent; } @@ -435,7 +448,7 @@ void load(HashMap content, SparseArray<@NullableType Stri /** * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such - * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}. + * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}. * * @param content The key to content map to persist. * @throws IOException If an error occurs persisting the index. @@ -453,8 +466,10 @@ void load(HashMap content, SparseArray<@NullableType Stri * Called when a {@link CachedContent} is removed. * * @param cachedContent The removed {@link CachedContent}. + * @param neverStored True if the {@link CachedContent} was added more recently than when the + * index was last stored. */ - void onRemove(CachedContent cachedContent); + void onRemove(CachedContent cachedContent, boolean neverStored); } /** {@link Storage} implementation that uses an {@link AtomicFile}. */ @@ -540,7 +555,7 @@ public void onUpdate(CachedContent cachedContent) { } @Override - public void onRemove(CachedContent cachedContent) { + public void onRemove(CachedContent cachedContent, boolean neverStored) { changed = true; } @@ -856,8 +871,12 @@ public void onUpdate(CachedContent cachedContent) { } @Override - public void onRemove(CachedContent cachedContent) { - pendingUpdates.put(cachedContent.id, null); + public void onRemove(CachedContent cachedContent, boolean neverStored) { + if (neverStored) { + pendingUpdates.delete(cachedContent.id); + } else { + pendingUpdates.put(cachedContent.id, null); + } } private Cursor getCursor() { From 90cb157985891d3cabb20a86965b9dccf003ba1b Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 5 May 2019 17:58:33 +0100 Subject: [PATCH 058/219] Update translations PiperOrigin-RevId: 246729123 --- library/ui/src/main/res/values-af/strings.xml | 2 +- library/ui/src/main/res/values-cs/strings.xml | 2 +- library/ui/src/main/res/values-hi/strings.xml | 4 ++-- library/ui/src/main/res/values-hy/strings.xml | 2 +- library/ui/src/main/res/values-ja/strings.xml | 2 +- library/ui/src/main/res/values-ka/strings.xml | 2 +- library/ui/src/main/res/values-sw/strings.xml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 9e0fc245fc7..8a983c543a9 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -21,7 +21,7 @@ Verwyder tans aflaaie Video Oudio - SMS + Teks Geen Outo Onbekend diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 8c73c01d742..1568074f9f2 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -21,7 +21,7 @@ Odstraňování staženého obsahu Videa Zvuk - SMS + Text Žádné Automaticky Neznámé diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index da606cd1666..8ba92054ffd 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -31,8 +31,8 @@ सराउंड साउंड 5.1 सराउंड साउंड 7.1 सराउंड साउंड - वैकल्पिक - अतिरिक्त + विकल्प + सप्लिमेंट्री कमेंट्री सबटाइटल %1$.2f एमबीपीएस diff --git a/library/ui/src/main/res/values-hy/strings.xml b/library/ui/src/main/res/values-hy/strings.xml index 11a9124f543..04a2aeb1402 100644 --- a/library/ui/src/main/res/values-hy/strings.xml +++ b/library/ui/src/main/res/values-hy/strings.xml @@ -35,6 +35,6 @@ Լրացուցիչ Մեկնաբանություններ Ենթագրեր - %1$.2f մբ/վ + %1$.2f Մբիթ/վ %1$s, %2$s diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index aef5a12a960..b4158736a81 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -21,7 +21,7 @@ ダウンロードを削除しています 動画 音声 - SMS + 文字 なし 自動 不明 diff --git a/library/ui/src/main/res/values-ka/strings.xml b/library/ui/src/main/res/values-ka/strings.xml index f7b8272bcca..13ceaaf51f9 100644 --- a/library/ui/src/main/res/values-ka/strings.xml +++ b/library/ui/src/main/res/values-ka/strings.xml @@ -21,7 +21,7 @@ მიმდინარეობს ჩამოტვირთვების ამოშლა ვიდეო აუდიო - SMS + ტექსტი არცერთი ავტომატური უცნობი diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index af58d417d69..1cdd3252780 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -21,7 +21,7 @@ Inaondoa vipakuliwa Video Sauti - SMS + Maandishi Hamna Otomatiki Haijulikani From 7d430423d7bf37a87561af2d084f4655ecb636be Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 5 May 2019 19:42:42 +0100 Subject: [PATCH 059/219] Merge pull request #5760 from matamegger:feature/hex_format_tags_in_url_template PiperOrigin-RevId: 246733842 --- .../android/exoplayer2/source/dash/manifest/UrlTemplate.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java index a7ce7eb9a0d..7d139936553 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java @@ -139,7 +139,10 @@ private static int parseTemplate(String template, String[] urlPieces, int[] iden String formatTag = DEFAULT_FORMAT_TAG; if (formatTagIndex != -1) { formatTag = identifier.substring(formatTagIndex); - if (!formatTag.endsWith("d")) { + // Allowed conversions are decimal integer (which is the only conversion allowed by the + // DASH specification) and hexadecimal integer (due to existing content that uses it). + // Else we assume that the conversion is missing, and that it should be decimal integer. + if (!formatTag.endsWith("d") && !formatTag.endsWith("x")) { formatTag += "d"; } identifier = identifier.substring(0, formatTagIndex); From b626dd70c3445e921a63b5c3cf4797472378ac52 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 25 Apr 2019 16:52:35 +0100 Subject: [PATCH 060/219] Add DownloadHelper.createMediaSource utility method PiperOrigin-RevId: 245243488 --- .../exoplayer2/demo/DownloadTracker.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 29 ++-- library/core/proguard-rules.txt | 9 +- .../exoplayer2/offline/DownloadHelper.java | 126 +++++++++++------- 4 files changed, 98 insertions(+), 75 deletions(-) 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 f372a47df6f..a913a9b891f 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.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 boolean isDownloaded(Uri uri) { } @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 acb24adebe7..35307eb5d8e 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.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.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 @@ private MediaSource buildMediaSource(Uri uri) { } 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/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 07ba4381827..8c118105065 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/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 8a15c82c893..755f7e03432 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.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.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.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 interface Callback { 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 static DownloadHelper forDash( 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 static DownloadHelper forHls( 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 static DownloadHelper forSmoothStreaming( 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 @@ private TrackSelectorResult runTrackSelection(int periodIndex) { } } - 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); + } } } From 0698bd1dbb5c4a8bf7f8253e5321dd56078226be Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 May 2019 18:05:26 +0100 Subject: [PATCH 061/219] Add option to clear all downloads. Adding an explicit option to clear all downloads prevents repeated database access in a loop when trying to delete all downloads. However, we still create an arbitrary number of parallel Task threads for this and seperate callbacks for each download. PiperOrigin-RevId: 247234181 --- RELEASENOTES.md | 4 ++ .../offline/DefaultDownloadIndex.java | 13 ++++ .../exoplayer2/offline/DownloadManager.java | 71 +++++++++++++++---- .../exoplayer2/offline/DownloadService.java | 39 ++++++++++ .../offline/WritableDownloadIndex.java | 7 ++ .../offline/DownloadManagerTest.java | 26 +++++++ 6 files changed, 146 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e69bcc9173..310b947fdd6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### 2.10.1 ### + +* Offline: Add option to remove all downloads. + ### 2.10.0 ### * Core library: 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 06f308d1e93..ef4bd00f20b 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 void setDownloadingStatesToQueued() throws DatabaseIOException { } } + @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/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 3bf03dd3e83..ec5ff81d97e 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 @@ default void onRequirementsStateChanged( 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 void removeDownload(String id) { 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 void handleMessage(Message message) { 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 @@ private void removeDownload(String id) { 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 @@ private Download putDownloadWithState(Download download, @Download.State int sta // 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 @@ private int getDownloadIndex(String id) { 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 fdd7163a2c8..3900dc8e938 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 static Intent buildRemoveDownloadIntent( .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 static void sendRemoveDownload( 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 int onStartCommand(Intent intent, int flags, int startId) { 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 ae634f8544b..dc7085c85e0 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/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 2b9ef11235a..de430d1416a 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 void secondSameRemoveRequestIgnored() throws Throwable { 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 @@ private DownloadRunner postRemoveRequest() { return this; } + private DownloadRunner postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + return this; + } + private DownloadRunner postDownloadRequest(StreamKey... keys) { DownloadRequest downloadRequest = new DownloadRequest( From 85a86e434a6aa4be083afe38130818865622d061 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 04:40:24 +0100 Subject: [PATCH 062/219] Increase gapless trim sample count PiperOrigin-RevId: 247348352 --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0185a6d8af2..6fb0ac68564 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 @@ * 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"); From ee5981c02dc1e6c465a463c2f8d826963619149b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 May 2019 14:40:51 +0100 Subject: [PATCH 063/219] Ensure messages get deleted if they throw an exception. If a PlayerMessage throws an exception, it is currently not deleted from the list of pending messages. This may be problematic as the list of pending messages is kept when the player is retried without reset and the message is sent again in such a case. PiperOrigin-RevId: 247414494 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 37774bccb55..03c3482eac1 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 @@ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPerio && 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() From 29add854af1b9ad2a645a229da8c601807731d52 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 15:10:41 +0100 Subject: [PATCH 064/219] Update player accessed on wrong thread URL PiperOrigin-RevId: 247418601 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 910404a8759..697f35e4172 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 @@ private void verifyApplicationThread() { 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; From ac07c56dab4b5d90f17731d3b5e878a9b154206a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 16:23:02 +0100 Subject: [PATCH 065/219] Fix NPE in HLS deriveAudioFormat. Issue:#5868 PiperOrigin-RevId: 247613811 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 310b947fdd6..4f05b0a78d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.10.1 ### +* Fix NPE when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Offline: Add option to remove all downloads. ### 2.10.0 ### 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 ef233bb5669..2cfd14c79da 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 @@ private static Format deriveAudioFormat( if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - roleFlags = mediaTagFormat.roleFlags; + roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; } From 6ead14880bea2471add1fcea1f8fa026d06d7a61 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 17:13:36 +0100 Subject: [PATCH 066/219] Add setCodecOperatingRate workaround for 48KHz audio on ZTE Axon7 mini. Issue:#5821 PiperOrigin-RevId: 247621164 --- RELEASENOTES.md | 2 ++ .../exoplayer2/audio/MediaCodecAudioRenderer.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4f05b0a78d9..4ee6c644446 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Offline: Add option to remove all downloads. +* 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 ### 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 07769e7d85c..e75f7ffc7b3 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 @@ protected MediaFormat getMediaFormat( // 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 @@ private void updateCurrentPosition() { } } + /** + * 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. From 1b9d018296ae5c2a6fa6bf23ed9b563d12e804c5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 18:12:05 +0100 Subject: [PATCH 067/219] Fix Javadoc links. PiperOrigin-RevId: 247630389 --- .../exoplayer2/analytics/AnalyticsListener.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 7f74216cc8c..3400cf25b69 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 @@ final class EventTime { 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 @@ final class EventTime { 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 @@ final class EventTime { * @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. From bef386bea8c9fe4ef3e765a46503a49a0401d1ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 11:56:35 +0100 Subject: [PATCH 068/219] Increase gradle heap size The update to Gradle 5.1.1 decreased the default heap size to 512MB and our build runs into Out-of-Memory errors. Setting the gradle flags to higher values instead. See https://developer.android.com/studio/releases/gradle-plugin#3-4-0 PiperOrigin-RevId: 247908526 --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index 4b9bfa8fa2d..31ff0ad6b6e 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 From 48de1010a8ca84dcc89c0f6c139d644719acc6e0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 May 2019 16:05:34 +0100 Subject: [PATCH 069/219] Allow line terminators in ICY metadata Issue: #5876 PiperOrigin-RevId: 247935822 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4ee6c644446..6a49f911d7b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). 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 d04cd3a9994..489719eda40 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/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 9cbcea5814e..97aac9995d6 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 @@ -70,6 +70,17 @@ public void decode_quoteInTitle() { 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(); From 035686e58cd45916aac05e15e6c24f30350a77b6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 16:21:33 +0100 Subject: [PATCH 070/219] Fix Javadoc generation. Accessing task providers (like javaCompileProvider) at sync time is not possible. That's why the source sets of all generateJavadoc tasks is empty. The set of source directories can also be accessed directly through the static sourceSets field. Combining these allows to statically provide the relevant source files to the javadoc task without needing to access the run-time task provider. PiperOrigin-RevId: 247938176 --- javadoc_library.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/javadoc_library.gradle b/javadoc_library.gradle index a818ea390e2..74fcc3dd6c3 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", From cea3071b333618161d09931ee4dcab3d14fa3125 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 May 2019 12:33:19 +0100 Subject: [PATCH 071/219] Fix rendering DVB subtitle on API 28. Issue: #5862 PiperOrigin-RevId: 248112524 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/text/dvb/DvbParser.java | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a49f911d7b..55349ad42e5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). ### 2.10.0 ### 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 eb956f06db1..3f2fef454fa 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.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 @@ public List decode(byte[] data, int limit) { 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 @@ public List decode(byte[] data, int limit) { 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 @@ public List decode(byte[] data, int limit) { (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. From 3ce0d89c56fa8d0a53b3ed82bd4dc67e58ef877a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 May 2019 13:42:16 +0100 Subject: [PATCH 072/219] Allow empty values in ICY metadata Issue: #5876 PiperOrigin-RevId: 248119726 --- RELEASENOTES.md | 2 +- .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55349ad42e5..fa2baceac36 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,7 +4,7 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). -* Fix handling of line terminators in SHOUTcast ICY metadata +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Offline: Add option to remove all downloads. * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 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 489719eda40..3d873926bbe 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("(.+?)='(.+?)';", Pattern.DOTALL); + 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/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 97aac9995d6..4602d172a66 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 void decode_titleOnly() { 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(); From 6e9df31e7d432d1c1c5196a35cfcd26d12bd8bef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 14 May 2019 23:18:42 +0100 Subject: [PATCH 073/219] Add links to the developer guide in some READMEs PiperOrigin-RevId: 248221982 --- library/dash/README.md | 2 ++ library/hls/README.md | 2 ++ library/smoothstreaming/README.md | 2 ++ library/ui/README.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/library/dash/README.md b/library/dash/README.md index 7831033b99d..1076716684c 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 1dd1b7a62ea..3470c29e3cd 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/smoothstreaming/README.md b/library/smoothstreaming/README.md index 4fa24543d62..d53471d17ca 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 341ea2fb16d..16136b3d945 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 From 7f89fa9a8ce63d56f840352c79e7360e445d5402 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 May 2019 17:50:50 +0100 Subject: [PATCH 074/219] Add simpler HttpDataSource constructors PiperOrigin-RevId: 248350557 --- .../ext/cronet/CronetDataSource.java | 24 +++++++++++++++---- .../ext/okhttp/OkHttpDataSource.java | 9 +++++++ .../upstream/DefaultHttpDataSource.java | 5 ++++ .../exoplayer2/testutil/FakeMediaChunk.java | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) 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 a9995af0e41..ca196b1d2f9 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { * #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 CronetDataSource( public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public CronetDataSource( public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public CronetDataSource( /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, 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 a749495184d..8eb8bba920f 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/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 6aad517004b..66036b7a841 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/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 6669504c07b..fd7be241dfb 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 @@ /** 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); From 8efaf5fd7d5bdf1f55f35109a43380e8f7f6be0b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 075/219] don't call stop before preparing the player Issue: #5891 PiperOrigin-RevId: 248369509 --- .../mediasession/MediaSessionConnector.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 9c80fabc50a..06f1cee001d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -834,10 +834,9 @@ private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } - private void stopPlayerForPrepare(boolean playWhenReady) { + private void setPlayWhenReady(boolean playWhenReady) { if (player != null) { - player.stop(); - player.setPlayWhenReady(playWhenReady); + controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); } } @@ -1052,14 +1051,14 @@ public void onPlay() { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); } } @@ -1182,7 +1181,7 @@ public void onCommand(String command, Bundle extras, ResultReceiver cb) { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1190,7 +1189,7 @@ public void onPrepare() { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1198,7 +1197,7 @@ public void onPrepareFromMediaId(String mediaId, Bundle extras) { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1206,7 +1205,7 @@ public void onPrepareFromSearch(String query, Bundle extras) { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1214,7 +1213,7 @@ public void onPrepareFromUri(Uri uri, Bundle extras) { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1222,7 +1221,7 @@ public void onPlayFromMediaId(String mediaId, Bundle extras) { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1230,7 +1229,7 @@ public void onPlayFromSearch(String query, Bundle extras) { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From 6e581f5270f5cfa9f09633ae83daefa62d83152d Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 076/219] Revert "don't call stop before preparing the player" This reverts commit 8efaf5fd7d5bdf1f55f35109a43380e8f7f6be0b. --- .../mediasession/MediaSessionConnector.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 06f1cee001d..9c80fabc50a 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -834,9 +834,10 @@ private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } - private void setPlayWhenReady(boolean playWhenReady) { + private void stopPlayerForPrepare(boolean playWhenReady) { if (player != null) { - controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); + player.stop(); + player.setPlayWhenReady(playWhenReady); } } @@ -1051,14 +1052,14 @@ public void onPlay() { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - setPlayWhenReady(/* playWhenReady= */ true); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - setPlayWhenReady(/* playWhenReady= */ false); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); } } @@ -1181,7 +1182,7 @@ public void onCommand(String command, Bundle extras, ResultReceiver cb) { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1189,7 +1190,7 @@ public void onPrepare() { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1197,7 +1198,7 @@ public void onPrepareFromMediaId(String mediaId, Bundle extras) { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1205,7 +1206,7 @@ public void onPrepareFromSearch(String query, Bundle extras) { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1213,7 +1214,7 @@ public void onPrepareFromUri(Uri uri, Bundle extras) { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1221,7 +1222,7 @@ public void onPlayFromMediaId(String mediaId, Bundle extras) { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1229,7 +1230,7 @@ public void onPlayFromSearch(String query, Bundle extras) { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ true); + stopPlayerForPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From 9e4b89d1cb21a97c230321311cf0446540726249 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 16 May 2019 11:42:05 +0100 Subject: [PATCH 077/219] Ignore empty timelines in ImaAdsLoader. We previously only checked whether the reason for the timeline change is RESET which indicates an empty timeline. Change this to an explicit check for empty timelines to also ignore empty media or intermittent timeline changes to an empty timeline which are not marked as RESET. Issue:#5831 PiperOrigin-RevId: 248499118 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 465ad51ac5b..f1316b2bfb0 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 void resumeAd() { @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); From 15b319cba24fcca91c18de6a111b0994651bbee1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 May 2019 12:30:13 +0100 Subject: [PATCH 078/219] Bump release to 2.10.1 and update release notes PiperOrigin-RevId: 248503235 --- RELEASENOTES.md | 8 ++++---- constants.gradle | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fa2baceac36..9e7a992e111 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,15 +2,15 @@ ### 2.10.1 ### -* Fix NPE when using HLS chunkless preparation +* 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)). -* Offline: Add option to remove all downloads. -* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing - 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). * 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 ### diff --git a/constants.gradle b/constants.gradle index 5063c59141a..b2ee322ee69 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/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 72760db31bb..a90435227bb 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} From 9ec330e7c771d33b8cb7ac043eb52aee4af4b316 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 May 2019 12:38:07 +0100 Subject: [PATCH 079/219] Fix platform scheduler javadoc PiperOrigin-RevId: 248503971 --- .../google/android/exoplayer2/scheduler/PlatformScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8572c9c7ca1..e6679e1a5aa 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 @@ * * * - * * } From 6fa58f8d695657f901f59c9e4d2807c7949a33ce Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 22 May 2019 19:45:48 +0100 Subject: [PATCH 080/219] Update issue template for bugs --- .github/ISSUE_TEMPLATE/bug.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 690069ffa8e..a4996278bd1 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -8,9 +8,12 @@ assignees: '' Before filing a bug: ----------------------- -- Search existing issues, including issues that are closed. -- Consult our FAQs, supported devices and supported formats pages. These can be - found at https://exoplayer.dev/. +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats and devices. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger - Rule out issues in your own code. A good way to do this is to try and reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: From a5d18f3fa73c749b14e72f84302d76e065180aa3 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 22 May 2019 19:47:45 +0100 Subject: [PATCH 081/219] Update issue template for content_not_playing --- .github/ISSUE_TEMPLATE/content_not_playing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index f326e7cd46c..ff29f3a7d1c 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -8,9 +8,12 @@ assignees: '' Before filing a content issue: ------------------------------ -- Search existing issues, including issues that are closed. +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue - Consult our supported formats page, which can be found at https://exoplayer.dev/supported-formats.html. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger - Try playing your content in the ExoPlayer demo app. Information about the ExoPlayer demo app can be found here: http://exoplayer.dev/demo-application.html. From 762a13253703456b8c5b4641031898022ef62882 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 22 May 2019 19:48:45 +0100 Subject: [PATCH 082/219] Update issue template for feature requests --- .github/ISSUE_TEMPLATE/feature_request.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 089de359109..d481de33cea 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,8 +8,9 @@ assignees: '' Before filing a feature request: ----------------------- -- Search existing open issues, specifically with the label ‘enhancement’. -- Search existing pull requests. +- Search existing open issues, specifically with the label ‘enhancement’: + https://github.com/google/ExoPlayer/labels/enhancement +- Search existing pull requests: https://github.com/google/ExoPlayer/pulls When filing a feature request: ----------------------- From ecb7b8758cd6a700b72275a56d22b3dc56959764 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 22 May 2019 19:50:10 +0100 Subject: [PATCH 083/219] Update issue template for questions --- .github/ISSUE_TEMPLATE/question.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 3ed569862fd..a68e4e70e14 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -12,8 +12,12 @@ Before filing a question: a general Android development question, please do so on Stack Overflow. - Search existing issues, including issues that are closed. It’s often the quickest way to get an answer! -- Consult our FAQs, developer guide and the class reference of ExoPlayer. These - can be found at https://exoplayer.dev/. + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats, devices as well as + information about how to use the ExoPlayer library. +- The ExoPlayer library Javadoc can be found at + https://exoplayer.dev/doc/reference/ When filing a question: ----------------------- From 0a5a8f547f076f7c64e142004c8c1184b19e2191 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 084/219] don't call stop before preparing the player Issue: #5891 PiperOrigin-RevId: 248369509 --- .../mediasession/MediaSessionConnector.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 9c80fabc50a..06f1cee001d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -834,10 +834,9 @@ private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } - private void stopPlayerForPrepare(boolean playWhenReady) { + private void setPlayWhenReady(boolean playWhenReady) { if (player != null) { - player.stop(); - player.setPlayWhenReady(playWhenReady); + controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); } } @@ -1052,14 +1051,14 @@ public void onPlay() { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); } } @@ -1182,7 +1181,7 @@ public void onCommand(String command, Bundle extras, ResultReceiver cb) { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1190,7 +1189,7 @@ public void onPrepare() { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1198,7 +1197,7 @@ public void onPrepareFromMediaId(String mediaId, Bundle extras) { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1206,7 +1205,7 @@ public void onPrepareFromSearch(String query, Bundle extras) { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1214,7 +1213,7 @@ public void onPrepareFromUri(Uri uri, Bundle extras) { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1222,7 +1221,7 @@ public void onPlayFromMediaId(String mediaId, Bundle extras) { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1230,7 +1229,7 @@ public void onPlayFromSearch(String query, Bundle extras) { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From e961def004243546830770cc0f3b9fe4725bf7e6 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 May 2019 17:29:32 +0100 Subject: [PATCH 085/219] Add playWhenReady to prepareXyz methods of PlaybackPreparer. Issue: #5891 PiperOrigin-RevId: 248541827 --- RELEASENOTES.md | 7 ++ .../mediasession/MediaSessionConnector.java | 73 +++++++++++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e7a992e111..7d7085af249 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,12 @@ # Release notes # +### 2.10.2 ### + +* Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods + to indicate whether a controller sent a play or only a prepare command. This + allows to take advantage of decoder reuse with the MediaSessionConnector + ([#5891](https://github.com/google/ExoPlayer/issues/5891)). + ### 2.10.1 ### * Offline: Add option to remove all downloads. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 06f1cee001d..c0b5fd67f6a 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -172,7 +172,7 @@ boolean onCommand( ResultReceiver cb); } - /** Interface to which playback preparation actions are delegated. */ + /** Interface to which playback preparation and play actions are delegated. */ public interface PlaybackPreparer extends CommandReceiver { long ACTIONS = @@ -197,14 +197,36 @@ public interface PlaybackPreparer extends CommandReceiver { * @return The bitmask of the supported media actions. */ long getSupportedPrepareActions(); - /** See {@link MediaSessionCompat.Callback#onPrepare()}. */ - void onPrepare(); - /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */ - void onPrepareFromMediaId(String mediaId, Bundle extras); - /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */ - void onPrepareFromSearch(String query, Bundle extras); - /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ - void onPrepareFromUri(Uri uri, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onPrepare()}. + * + * @param playWhenReady Whether playback should be started after preparation. + */ + void onPrepare(boolean playWhenReady); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. + * + * @param mediaId The media id of the media item to be prepared. + * @param playWhenReady Whether playback should be started after preparation. + * @param extras A {@link Bundle} of extras passed by the media controller. + */ + void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. + * + * @param query The search query. + * @param playWhenReady Whether playback should be started after preparation. + * @param extras A {@link Bundle} of extras passed by the media controller. + */ + void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. + * + * @param uri The {@link Uri} of the media item to be prepared. + * @param playWhenReady Whether playback should be started after preparation. + * @param extras A {@link Bundle} of extras passed by the media controller. + */ + void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras); } /** @@ -834,12 +856,6 @@ private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } - private void setPlayWhenReady(boolean playWhenReady) { - if (player != null) { - controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); - } - } - private void rewind(Player player) { if (player.isCurrentWindowSeekable() && rewindMs > 0) { seekTo(player, player.getCurrentPosition() - rewindMs); @@ -1046,19 +1062,19 @@ public void onPlay() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) { if (player.getPlaybackState() == Player.STATE_IDLE) { if (playbackPreparer != null) { - playbackPreparer.onPrepare(); + playbackPreparer.onPrepare(/* playWhenReady= */ true); } } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } - setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - setPlayWhenReady(/* playWhenReady= */ false); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); } } @@ -1181,56 +1197,49 @@ public void onCommand(String command, Bundle extras, ResultReceiver cb) { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - setPlayWhenReady(/* playWhenReady= */ false); - playbackPreparer.onPrepare(); + playbackPreparer.onPrepare(/* playWhenReady= */ false); } } @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ false); - playbackPreparer.onPrepareFromMediaId(mediaId, extras); + playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras); } } @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ false); - playbackPreparer.onPrepareFromSearch(query, extras); + playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras); } } @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ false); - playbackPreparer.onPrepareFromUri(uri, extras); + playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras); } } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - setPlayWhenReady(/* playWhenReady= */ true); - playbackPreparer.onPrepareFromMediaId(mediaId, extras); + playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras); } } @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - setPlayWhenReady(/* playWhenReady= */ true); - playbackPreparer.onPrepareFromSearch(query, extras); + playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras); } } @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - setPlayWhenReady(/* playWhenReady= */ true); - playbackPreparer.onPrepareFromUri(uri, extras); + playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras); } } From b4d72d12f7cb90c2d5693f1008d036f3af7a8323 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 20 May 2019 17:48:15 +0100 Subject: [PATCH 086/219] Add ProgressUpdateListener Issue: #5834 PiperOrigin-RevId: 249067445 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ui/PlayerControlView.java | 27 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7d7085af249..527f906405b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). +* Add ProgressUpdateListener to PlayerControlView + ([#5834](https://github.com/google/ExoPlayer/issues/5834)). ### 2.10.1 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index a5deb808c1d..0b83615807b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -188,6 +188,18 @@ public interface VisibilityListener { void onVisibilityChange(int visibility); } + /** Listener to be notified when progress has been updated. */ + public interface ProgressUpdateListener { + + /** + * Called when progress needs to be updated. + * + * @param position The current position. + * @param bufferedPosition The current buffered position. + */ + void onProgressUpdate(long position, long bufferedPosition); + } + /** The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; /** The default rewind increment, in milliseconds. */ @@ -235,7 +247,8 @@ public interface VisibilityListener { @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; - private VisibilityListener visibilityListener; + @Nullable private VisibilityListener visibilityListener; + @Nullable private ProgressUpdateListener progressUpdateListener; @Nullable private PlaybackPreparer playbackPreparer; private boolean isAttachedToWindow; @@ -454,6 +467,15 @@ public void setVisibilityListener(VisibilityListener listener) { this.visibilityListener = listener; } + /** + * Sets the {@link ProgressUpdateListener}. + * + * @param listener The listener to be notified about when progress is updated. + */ + public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) { + this.progressUpdateListener = listener; + } + /** * Sets the {@link PlaybackPreparer}. * @@ -855,6 +877,9 @@ private void updateProgress() { timeBar.setPosition(position); timeBar.setBufferedPosition(bufferedPosition); } + if (progressUpdateListener != null) { + progressUpdateListener.onProgressUpdate(position, bufferedPosition); + } // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); From e4d66c4105bf8c74f7e49e0dd4e8c77f5c66228f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 May 2019 17:53:08 +0100 Subject: [PATCH 087/219] Update a reference to SimpleExoPlayerView PiperOrigin-RevId: 249068395 --- library/ui/src/main/res/values/attrs.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index f4a7976ebd2..27e6a5b3b84 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -24,7 +24,7 @@ - + From 1d0618ee1abf5a78465c86a2410bddab8112dbe6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 May 2019 15:02:16 +0100 Subject: [PATCH 088/219] Update surface directly from SphericalSurfaceView The SurfaceListener just sets the surface on the VideoComponent, but SphericalSurfaceView already accesses the VideoComponent directly so it seems simpler to update the surface directly. PiperOrigin-RevId: 249242185 --- .../android/exoplayer2/ui/PlayerView.java | 16 ---------- .../ui/spherical/SphericalSurfaceView.java | 32 +++---------------- 2 files changed, 4 insertions(+), 44 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 93461c1b249..a38d61b1b1b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -35,7 +35,6 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; @@ -50,7 +49,6 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.VideoComponent; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -405,7 +403,6 @@ public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { break; case SURFACE_TYPE_MONO360_VIEW: SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); - sphericalSurfaceView.setSurfaceListener(componentListener); sphericalSurfaceView.setSingleTapListener(componentListener); surfaceView = sphericalSurfaceView; break; @@ -1359,7 +1356,6 @@ private final class ComponentListener TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener, SingleTapListener { // TextOutput implementation @@ -1449,18 +1445,6 @@ public void onLayoutChange( applyTextureViewRotation((TextureView) view, textureViewRotation); } - // SphericalSurfaceView.SurfaceTextureListener implementation - - @Override - public void surfaceChanged(@Nullable Surface surface) { - if (player != null) { - VideoComponent videoComponent = player.getVideoComponent(); - if (videoComponent != null) { - videoComponent.setVideoSurface(surface); - } - } - } - // SingleTapListener implementation @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index 1029a28323f..02b30436656 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -53,20 +53,6 @@ */ public final class SphericalSurfaceView extends GLSurfaceView { - /** - * This listener can be used to be notified when the {@link Surface} associated with this view is - * changed. - */ - public interface SurfaceListener { - /** - * Invoked when the surface is changed or there isn't one anymore. Any previous surface - * shouldn't be used after this call. - * - * @param surface The new surface or null if there isn't one anymore. - */ - void surfaceChanged(@Nullable Surface surface); - } - // Arbitrary vertical field of view. private static final int FIELD_OF_VIEW_DEGREES = 90; private static final float Z_NEAR = .1f; @@ -84,7 +70,6 @@ public interface SurfaceListener { private final Handler mainHandler; private final TouchTracker touchTracker; private final SceneRenderer scene; - private @Nullable SurfaceListener surfaceListener; private @Nullable SurfaceTexture surfaceTexture; private @Nullable Surface surface; private @Nullable Player.VideoComponent videoComponent; @@ -156,15 +141,6 @@ public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) } } - /** - * Sets the {@link SurfaceListener} used to listen to surface events. - * - * @param listener The listener for surface events. - */ - public void setSurfaceListener(@Nullable SurfaceListener listener) { - surfaceListener = listener; - } - /** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */ public void setSingleTapListener(@Nullable SingleTapListener listener) { touchTracker.setSingleTapListener(listener); @@ -196,8 +172,8 @@ protected void onDetachedFromWindow() { mainHandler.post( () -> { if (surface != null) { - if (surfaceListener != null) { - surfaceListener.surfaceChanged(null); + if (videoComponent != null) { + videoComponent.clearVideoSurface(surface); } releaseSurface(surfaceTexture, surface); surfaceTexture = null; @@ -214,8 +190,8 @@ private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { Surface oldSurface = this.surface; this.surfaceTexture = surfaceTexture; this.surface = new Surface(surfaceTexture); - if (surfaceListener != null) { - surfaceListener.surfaceChanged(surface); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); } releaseSurface(oldSurfaceTexture, oldSurface); }); From 94c10f1984b9197ba37f42ce81974cc5001e1bca Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 16:05:56 +0100 Subject: [PATCH 089/219] Propagate attributes to DefaultTimeBar Issue: #5765 PiperOrigin-RevId: 249251150 --- RELEASENOTES.md | 3 + .../android/exoplayer2/ui/DefaultTimeBar.java | 27 ++++-- .../exoplayer2/ui/PlayerControlView.java | 32 ++++++- .../android/exoplayer2/ui/PlayerView.java | 17 ++-- .../res/layout/exo_playback_control_view.xml | 3 +- library/ui/src/main/res/values/attrs.xml | 88 ++++++++++++++----- library/ui/src/main/res/values/ids.xml | 1 + 7 files changed, 135 insertions(+), 36 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 527f906405b..333fe5c3142 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### 2.10.2 ### +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 328b5d6a491..5c70203788f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -220,11 +220,26 @@ public class DefaultTimeBar extends View implements TimeBar { private @Nullable long[] adGroupTimesMs; private @Nullable boolean[] playedAdGroups; - /** Creates a new time bar. */ + public DefaultTimeBar(Context context) { + this(context, null); + } + + public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } + // Suppress warnings due to usage of View methods in the constructor. @SuppressWarnings("nullness:method.invocation.invalid") - public DefaultTimeBar(Context context, AttributeSet attrs) { - super(context, attrs); + public DefaultTimeBar( + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet timebarAttrs) { + super(context, attrs, defStyleAttr); seekBounds = new Rect(); progressBar = new Rect(); bufferedBar = new Rect(); @@ -251,9 +266,9 @@ public DefaultTimeBar(Context context, AttributeSet attrs) { int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP); int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP); int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP); - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0, - 0); + if (timebarAttrs != null) { + TypedArray a = + context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0); try { scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable); if (scrubberDrawable != null) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 0b83615807b..383d7966925 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -28,6 +28,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -97,6 +98,9 @@ *

  • Corresponding method: None *
  • Default: {@code R.layout.exo_player_control_view} * + *
  • All attributes that can be set on {@link DefaultTimeBar} can also be set on a + * PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the + * layout is overridden to specify a custom {@code exo_progress} (see below). * * *

    Overriding the layout file

    @@ -154,7 +158,15 @@ *
      *
    • Type: {@link TextView} *
    + *
  • {@code exo_progress_placeholder} - A placeholder that's replaced with the inflated + * {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists. + *
      + *
    • Type: {@link View} + *
    *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + * {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically + * propagated through to this instance. If a view exists with this id, any {@code + * exo_progress_placeholder} view will be ignored. *
      *
    • Type: {@link TimeBar} *
    @@ -330,9 +342,27 @@ public PlayerControlView( LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + TimeBar customTimeBar = findViewById(R.id.exo_progress); + View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder); + if (customTimeBar != null) { + timeBar = customTimeBar; + } else if (timeBarPlaceholder != null) { + // Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred, + // but standard attributes (e.g. background) are not. + DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs); + defaultTimeBar.setId(R.id.exo_progress); + defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent()); + int timeBarIndex = parent.indexOfChild(timeBarPlaceholder); + parent.removeView(timeBarPlaceholder); + parent.addView(defaultTimeBar, timeBarIndex); + timeBar = defaultTimeBar; + } else { + timeBar = null; + } durationView = findViewById(R.id.exo_duration); positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { timeBar.addListener(componentListener); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index a38d61b1b1b..8e94d967397 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -163,9 +163,10 @@ *
  • Corresponding method: None *
  • Default: {@code R.layout.exo_player_control_view} * - *
  • All attributes that can be set on a {@link PlayerControlView} can also be set on a - * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the - * layout is overridden to specify a custom {@code exo_controller} (see below). + *
  • All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can + * also be set on a PlayerView, and will be propagated to the inflated {@link + * PlayerControlView} unless the layout is overridden to specify a custom {@code + * exo_controller} (see below). * * *

    Overriding the layout file

    @@ -215,9 +216,10 @@ *
  • Type: {@link View} * *
  • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use - * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link + * DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated + * through to this instance. If a view exists with this id, any {@code + * exo_controller_placeholder} view will be ignored. *
      *
    • Type: {@link PlayerControlView} *
    @@ -456,8 +458,9 @@ public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { this.controller = customController; } else if (controllerPlaceholder != null) { // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. + // transferred, but standard attributes (e.g. background) are not. this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setId(R.id.exo_controller); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index ed2fb8e2b21..027e57ee928 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -76,8 +76,7 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 27e6a5b3b84..706fba0e0b7 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -31,18 +31,36 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -58,9 +76,11 @@ - + + - + + @@ -69,6 +89,20 @@ + + + + + + + + + + + + + + @@ -83,22 +117,36 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index e57301f9460..17b55cd731b 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -33,6 +33,7 @@ + From 7e587ae98f586ff9193189c395512b0f42ce39f9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 22 May 2019 09:17:49 +0100 Subject: [PATCH 090/219] Add missing annotations dependency Issue: #5926 PiperOrigin-RevId: 249404152 --- extensions/ima/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a91bbbd981a..2df9448d081 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,6 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From b1ff911e6a09a2ce9a4ba3e3c9f4c674b2557955 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 22 May 2019 11:27:57 +0100 Subject: [PATCH 091/219] Remove mistakenly left link in vp9 readme PiperOrigin-RevId: 249417898 --- extensions/vp9/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 0de29eea32a..2c5b64f8bd6 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -66,7 +66,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html -[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From cf93b8e73e2f996744247de54b554dc598892911 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 22 May 2019 14:54:41 +0100 Subject: [PATCH 092/219] Release DownloadHelper automatically if preparation failed. This prevents further unexpected updates if the MediaSource happens to finish its preparation at a later point. Issue:#5915 PiperOrigin-RevId: 249439246 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/offline/DownloadHelper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 333fe5c3142..6a46ffd5dc0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). ### 2.10.1 ### 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 755f7e03432..7e98f303013 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 @@ -951,6 +951,7 @@ private boolean handleDownloadHelperCallbackMessage(Message msg) { downloadHelper.onMediaPrepared(); return true; case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED: + release(); downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj)); return true; default: From 2e1ea379c3858f960c7e2402ac20722b43b2002d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 May 2019 10:56:58 +0100 Subject: [PATCH 093/219] Fix IndexOutOfBounds when there are no available codecs PiperOrigin-RevId: 249610014 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index f7855810d4c..be08186dc0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -53,7 +53,6 @@ import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -742,11 +741,11 @@ private void maybeInitCodecWithFallback( try { List allAvailableCodecInfos = getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder); + availableCodecInfos = new ArrayDeque<>(); if (enableDecoderFallback) { - availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos); - } else { - availableCodecInfos = - new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0))); + availableCodecInfos.addAll(allAvailableCodecInfos); + } else if (!allAvailableCodecInfos.isEmpty()) { + availableCodecInfos.add(allAvailableCodecInfos.get(0)); } preferredDecoderInitializationException = null; } catch (DecoderQueryException e) { From 9b104f6ec09e3ae0cd7cb6d4be52da8903f6149a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 May 2019 13:53:22 +0100 Subject: [PATCH 094/219] Reset upstream format when empty track selection happens PiperOrigin-RevId: 249819080 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 65039b93643..434b6c20114 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -322,6 +322,7 @@ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStre if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; + pendingResetUpstreamFormats = true; mediaChunks.clear(); if (loader.isLoading()) { if (sampleQueuesBuilt) { From 42ffc5215fc2c300b37246dbc47bbed109750316 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 12:20:14 +0100 Subject: [PATCH 095/219] Fix anchor usage in SubtitlePainter's setupBitmapLayout According to Cue's constructor (for bitmaps) documentation: + cuePositionAnchor does horizontal anchoring. + cueLineAnchor does vertical anchoring. Usage is currently inverted. Issue:#5633 PiperOrigin-RevId: 250253002 --- .../android/exoplayer2/ui/SubtitlePainter.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 4f22362de66..9ed1bbd0063 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -362,10 +362,16 @@ private void setupBitmapLayout() { int width = Math.round(parentWidth * cueSize); int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); - int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) - : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) - : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + int x = + Math.round( + cuePositionAnchor == Cue.ANCHOR_TYPE_END + ? (anchorX - width) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = + Math.round( + cueLineAnchor == Cue.ANCHOR_TYPE_END + ? (anchorY - height) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 5d72942a4927928d86f4587072faa1f750d6bf76 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 16:36:09 +0100 Subject: [PATCH 096/219] Fix VP9 build setup Update configuration script to use an external build, so we can remove use of isysroot which is broken in the latest NDK r19c. Also switch from gnustl_static to c++_static so that ndk-build with NDK r19c succeeds. Issue: #5922 PiperOrigin-RevId: 250287551 --- extensions/vp9/README.md | 3 +- extensions/vp9/src/main/jni/Application.mk | 4 +- .../jni/generate_libvpx_android_configs.sh | 44 ++++++------------- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 2c5b64f8bd6..be75eae3596 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. + The build configuration has been tested with Android NDK r19c. ``` NDK_PATH="" @@ -54,7 +55,7 @@ git checkout tags/v1.8.0 -b v1.8.0 ``` cd ${VP9_EXT_PATH}/jni && \ -./generate_libvpx_android_configs.sh "${NDK_PATH}" +./generate_libvpx_android_configs.sh ``` * Build the JNI native libraries from the command line: diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk index 59bf5f8f870..ed28f07acb1 100644 --- a/extensions/vp9/src/main/jni/Application.mk +++ b/extensions/vp9/src/main/jni/Application.mk @@ -15,6 +15,6 @@ # APP_OPTIM := release -APP_STL := gnustl_static +APP_STL := c++_static APP_CPPFLAGS := -frtti -APP_PLATFORM := android-9 +APP_PLATFORM := android-16 diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index eab68625556..18f1dd5c698 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -20,46 +20,33 @@ set -e -if [ $# -ne 1 ]; then - echo "Usage: ${0} " +if [ $# -ne 0 ]; then + echo "Usage: ${0}" exit fi -ndk="${1}" -shift 1 - # configuration parameters common to all architectures common_params="--disable-examples --disable-docs --enable-realtime-only" common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io" common_params+=" --disable-libyuv --disable-runtime-cpu-detect" +common_params+=" --enable-external-build" # configuration parameters for various architectures arch[0]="armeabi-v7a" -config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon" -config[0]+=" --enable-neon-asm" +config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm" -arch[1]="armeabi" -config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" -config[1]+=" --disable-neon-asm" +arch[1]="x86" +config[1]="--force-target=x86-android-gcc --disable-sse2" +config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" +config[1]+=" --disable-avx2 --enable-pic" -arch[2]="mips" -config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" +arch[2]="arm64-v8a" +config[2]="--force-target=armv8-android-gcc --enable-neon" -arch[3]="x86" -config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2" +arch[3]="x86_64" +config[3]="--force-target=x86_64-android-gcc --disable-sse2" config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[3]+=" --disable-avx2 --enable-pic" - -arch[4]="arm64-v8a" -config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon" - -arch[5]="x86_64" -config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" -config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" - -arch[6]="mips64" -config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk" +config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" limit=$((${#arch[@]} - 1)) @@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \ - -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \ - -isystem $ndk/sysroot/usr/include \ - " + ../../libvpx/configure ${config[${i}]} ${common_params} rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid From 8bc14bc2a9cb336956bc5a8c309bf7977c20efe3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 17:40:50 +0100 Subject: [PATCH 097/219] Allow enabling decoder fallback in DefaultRenderersFactory Also allow enabling decoder fallback with MediaCodecAudioRenderer. Issue: #5942 PiperOrigin-RevId: 250301422 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultRenderersFactory.java | 30 +++++++++++++- .../audio/MediaCodecAudioRenderer.java | 40 ++++++++++++++++++- .../testutil/DebugRenderersFactory.java | 1 + 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a46ffd5dc0..219d0fc23cb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,8 @@ * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). ### 2.10.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 2a977f5bba6..490d9613962 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory { @ExtensionRendererMode private int extensionRendererMode; private long allowedVideoJoiningTimeMs; private boolean playClearSamplesWithoutKeys; + private boolean enableDecoderFallback; private MediaCodecSelector mediaCodecSelector; /** @param context A {@link Context}. */ @@ -202,6 +204,19 @@ public DefaultRenderersFactory setPlayClearSamplesWithoutKeys( return this; } + /** + * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. + * This may result in using a decoder that is less efficient or slower than the primary decoder. + * + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) { + this.enableDecoderFallback = enableDecoderFallback; + return this; + } + /** * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers. * @@ -248,6 +263,7 @@ public Renderer[] createRenderers( mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, videoRendererEventListener, allowedVideoJoiningTimeMs, @@ -258,6 +274,7 @@ public Renderer[] createRenderers( mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, buildAudioProcessors(), eventHandler, audioRendererEventListener, @@ -282,6 +299,9 @@ public Renderer[] createRenderers( * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to @@ -294,6 +314,7 @@ protected void buildVideoRenderers( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, @@ -305,6 +326,7 @@ protected void buildVideoRenderers( allowedVideoJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -356,6 +378,9 @@ protected void buildVideoRenderers( * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers * before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. @@ -368,6 +393,7 @@ protected void buildAudioRenderers( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @@ -378,10 +404,10 @@ protected void buildAudioRenderers( mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, - AudioCapabilities.getCapabilities(context), - audioProcessors)); + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors))); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; 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 e75f7ffc7b3..a86eb97a372 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 @@ -245,12 +245,50 @@ public MediaCodecAudioRenderer( @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + audioSink); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + */ + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { super( C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - /* enableDecoderFallback= */ false, + enableDecoderFallback, /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 70059114dbf..92ec23c34d2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -55,6 +55,7 @@ protected void buildVideoRenderers( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, From 41ab7ef7c092b73e809963696463779750233b19 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 10:09:54 +0100 Subject: [PATCH 098/219] Fix video size reporting in surface YUV mode In surface YUV output mode the width/height fields of the VpxOutputBuffer were never populated. Fix this by adding a new method to set the width/height and calling it from JNI like we do for GL YUV mode. PiperOrigin-RevId: 250449734 --- .../android/exoplayer2/ext/vp9/VpxOutputBuffer.java | 13 +++++++++++-- extensions/vp9/src/main/jni/vpx_jni.cc | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 22330e0a053..30d7b8e92c1 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -60,8 +60,8 @@ public void release() { * Initializes the buffer. * * @param timeUs The presentation timestamp for the buffer, in microseconds. - * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link - * VpxDecoder#OUTPUT_MODE_YUV}. + * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link + * VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}. */ public void init(long timeUs, int mode) { this.timeUs = timeUs; @@ -110,6 +110,15 @@ public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, return true; } + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + private void initData(int size) { if (data == null || data.capacity() < size) { data = ByteBuffer.allocateDirect(size); diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 82c023afbc2..9fc8b09a18c 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -60,6 +60,7 @@ // JNI references for VpxOutputBuffer class. static jmethodID initForYuvFrame; +static jmethodID initForPrivateFrame; static jfieldID dataField; static jfieldID outputModeField; static jfieldID decoderPrivateField; @@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z"); + initForPrivateFrame = + env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V"); dataField = env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); @@ -602,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } jfb->d_w = img->d_w; jfb->d_h = img->d_h; + env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h); + if (env->ExceptionCheck()) { + return -1; + } env->SetIntField(jOutputBuffer, decoderPrivateField, id + kDecoderPrivateBase); } From 082aee692b5d42a8ce9c3da01e2eab2bc2ca3606 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 18:25:27 +0100 Subject: [PATCH 099/219] Allow passthrough of E-AC3-JOC streams PiperOrigin-RevId: 250517338 --- .../java/com/google/android/exoplayer2/C.java | 7 +++-- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- .../audio/MediaCodecAudioRenderer.java | 31 +++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 3 +- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 04a90b38d84..0120451bc1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -146,8 +146,8 @@ private C() {} * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link - * #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or - * {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, + * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -163,6 +163,7 @@ private C() {} ENCODING_PCM_A_LAW, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_E_AC3_JOC, ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, @@ -210,6 +211,8 @@ private C() {} public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; + /** @see AudioFormat#ENCODING_E_AC3_JOC */ + public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC; /** @see AudioFormat#ENCODING_AC4 */ public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; /** @see AudioFormat#ENCODING_DTS */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ffcd893e7b3..bd57c829161 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1125,6 +1125,7 @@ private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) case C.ENCODING_AC3: return 640 * 1000 / 8; case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: return 6144 * 1000 / 8; case C.ENCODING_AC4: return 2688 * 1000 / 8; @@ -1154,7 +1155,7 @@ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffe return DtsUtil.parseDtsAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC3) { return Ac3Util.getAc3SyncframeAudioSampleCount(); - } else if (encoding == C.ENCODING_E_AC3) { + } else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC4) { return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); 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 a86eb97a372..d43bd6cbf80 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 @@ -379,7 +379,7 @@ protected List getDecoderInfos( * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(int channelCount, String mimeType) { - return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType)); + return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID; } @Override @@ -475,11 +475,14 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) @C.Encoding int encoding; MediaFormat format; if (passthroughMediaFormat != null) { - encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); format = passthroughMediaFormat; + encoding = + getPassthroughEncoding( + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT), + format.getString(MediaFormat.KEY_MIME)); } else { - encoding = pcmEncoding; format = outputFormat; + encoding = pcmEncoding; } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); @@ -501,6 +504,28 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) } } + /** + * Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link + * C#ENCODING_INVALID} if passthrough is not possible. + */ + @C.Encoding + protected int getPassthroughEncoding(int channelCount, String mimeType) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) { + return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); + } + // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. + mimeType = MimeTypes.AUDIO_E_AC3; + } + + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + if (audioSink.supportsOutput(channelCount, encoding)) { + return encoding; + } else { + return C.ENCODING_INVALID; + } + } + /** * Called when the audio session id becomes known. The default implementation is a no-op. One * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e603f76dbc3..61457c308da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -348,8 +348,9 @@ public static int getTrackType(@Nullable String mimeType) { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: - case MimeTypes.AUDIO_E_AC3_JOC: return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_E_AC3_JOC: + return C.ENCODING_E_AC3_JOC; case MimeTypes.AUDIO_AC4: return C.ENCODING_AC4; case MimeTypes.AUDIO_DTS: From 9da9941e384322134f442ea93f3b0099ce37abdb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 29 May 2019 18:36:01 +0100 Subject: [PATCH 100/219] Fix TTML bitmap subtitles + Use start for anchoring, instead of center. + Add the height to the TTML bitmap cue rendering layout. Issue:#5633 PiperOrigin-RevId: 250519710 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/text/ttml/TtmlDecoder.java | 1 + .../google/android/exoplayer2/text/ttml/TtmlNode.java | 4 ++-- .../android/exoplayer2/text/ttml/TtmlRegion.java | 4 ++++ .../android/exoplayer2/text/ttml/TtmlDecoderTest.java | 10 +++++----- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 219d0fc23cb..8ea7feff29c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### 2.10.2 ### +* Subtitles: + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * UI: * Allow setting `DefaultTimeBar` attributes on `PlayerView` and `PlayerControlView`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index b39f467968d..6e0c495466c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -429,6 +429,7 @@ private TtmlRegion parseRegionAttributes( /* lineType= */ Cue.LINE_TYPE_FRACTION, lineAnchor, width, + height, /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, /* textSize= */ regionTextHeight); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index ecf5c8b0a0e..3b4d061aaaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -231,11 +231,11 @@ public List getCues( new Cue( bitmap, region.position, - Cue.ANCHOR_TYPE_MIDDLE, + Cue.ANCHOR_TYPE_START, region.line, region.lineAnchor, region.width, - /* height= */ Cue.DIMEN_UNSET)); + region.height)); } // Create text based cues. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index 2b1e9cf99af..3cbc25d4b24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -28,6 +28,7 @@ public final @Cue.LineType int lineType; public final @Cue.AnchorType int lineAnchor; public final float width; + public final float height; public final @Cue.TextSizeType int textSizeType; public final float textSize; @@ -39,6 +40,7 @@ public TtmlRegion(String id) { /* lineType= */ Cue.TYPE_UNSET, /* lineAnchor= */ Cue.TYPE_UNSET, /* width= */ Cue.DIMEN_UNSET, + /* height= */ Cue.DIMEN_UNSET, /* textSizeType= */ Cue.TYPE_UNSET, /* textSize= */ Cue.DIMEN_UNSET); } @@ -50,6 +52,7 @@ public TtmlRegion( @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width, + float height, int textSizeType, float textSize) { this.id = id; @@ -58,6 +61,7 @@ public TtmlRegion( this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; + this.height = height; this.textSizeType = textSizeType; this.textSize = textSize; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 000d0634ce6..85af6482c0e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -514,7 +514,7 @@ public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -524,7 +524,7 @@ public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce assertThat(cue.position).isEqualTo(21f / 100f); assertThat(cue.line).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(57f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(6f / 100f); cues = subtitle.getCues(7500000); assertThat(cues).hasSize(1); @@ -534,7 +534,7 @@ public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); } @Test @@ -549,7 +549,7 @@ public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException assertThat(cue.position).isEqualTo(307f / 1280f); assertThat(cue.line).isEqualTo(562f / 720f); assertThat(cue.size).isEqualTo(653f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(86f / 720f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -559,7 +559,7 @@ public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException assertThat(cue.position).isEqualTo(269f / 1280f); assertThat(cue.line).isEqualTo(612f / 720f); assertThat(cue.size).isEqualTo(730f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(43f / 720f); } @Test From 9860c486e0409f4c410cb28877e83cae85a7175e Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 11:54:15 +0100 Subject: [PATCH 101/219] Keep controller visible on d-pad key events PiperOrigin-RevId: 250661977 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerView.java | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ea7feff29c..acb22ab35d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,8 @@ * UI: * Allow setting `DefaultTimeBar` attributes on `PlayerView` and `PlayerControlView`. + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 8e94d967397..f92d550706d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -771,11 +771,20 @@ public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { return super.dispatchKeyEvent(event); } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - boolean handled = - isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - if (handled) { + + boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController; + boolean handled = false; + if (isDpadAndUseController && !controller.isVisible()) { + // Handle the key event by showing the controller. + maybeShowController(true); + handled = true; + } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) { + // The key event was handled as a media key or by the super class. We should also show the + // controller, or extend its show timeout if already visible. + maybeShowController(true); + handled = true; + } else if (isDpadAndUseController) { + // The key event wasn't handled, but we should extend the controller's show timeout. maybeShowController(true); } return handled; From 7cdcd89873e5874e3a33a97502dec3d2e2dd728a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 12:19:33 +0100 Subject: [PATCH 102/219] Update cast extension build PiperOrigin-RevId: 250664791 --- extensions/cast/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 4dc463ff81c..e067789bc41 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.1.2' + api 'com.google.android.gms:play-services-cast-framework:16.2.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') From d626e4bc54d0e78560cb411b452d1dcdd40c0b32 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:40:49 +0100 Subject: [PATCH 103/219] Rename host_activity.xml to avoid manifest merge conflicts. PiperOrigin-RevId: 250672752 --- .../com/google/android/exoplayer2/testutil/HostActivity.java | 3 ++- .../{host_activity.xml => exo_testutils_host_activity.xml} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename testutils/src/main/res/layout/{host_activity.xml => exo_testutils_host_activity.xml} (100%) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 73e8ac4f3ef..39429a8fa11 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -166,7 +166,8 @@ public void runTest( public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); + setContentView( + getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName())); surfaceView = findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); diff --git a/testutils/src/main/res/layout/host_activity.xml b/testutils/src/main/res/layout/exo_testutils_host_activity.xml similarity index 100% rename from testutils/src/main/res/layout/host_activity.xml rename to testutils/src/main/res/layout/exo_testutils_host_activity.xml From b9f3fd429d6c2e90d67e8e15103d9af22ff4cc43 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 15:08:51 +0100 Subject: [PATCH 104/219] Make parallel adaptive track selection more robust. Using parallel adaptation for Formats without bitrate information currently causes an exception. Handle this gracefully and also cases where all formats have the same bitrate. Issue:#5971 PiperOrigin-RevId: 250682127 --- RELEASENOTES.md | 3 +++ .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index acb22ab35d7..474570088f1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,9 @@ ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). ### 2.10.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index bbf57c56024..0adadd87c2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -757,7 +757,7 @@ private static double[][] getLogArrayValues(long[][] values) { for (int i = 0; i < values.length; i++) { logValues[i] = new double[values[i].length]; for (int j = 0; j < values[i].length; j++) { - logValues[i][j] = Math.log(values[i][j]); + logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]); } } return logValues; @@ -779,7 +779,8 @@ private static double[][] getSwitchPoints(double[][] logBitrates) { double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0]; for (int j = 0; j < logBitrates[i].length - 1; j++) { double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]); - switchPoints[i][j] = (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; + switchPoints[i][j] = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; } } return switchPoints; From 25e93a178adea0e54ca2954fbcc29c38c32e7131 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 25 Apr 2019 13:48:36 +0100 Subject: [PATCH 105/219] Toggle playback controls according to standard Android click handling. We currently toggle the view in onTouchEvent ACTION_DOWN which is non-standard and causes problems when used in a ViewGroup intercepting touch events. Switch to standard Android click handling instead which is also what most other player apps are doing. Issue:#5784 PiperOrigin-RevId: 245219728 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerView.java | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 474570088f1..90c3874cd7f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,8 @@ * UI: * Allow setting `DefaultTimeBar` attributes on `PlayerView` and `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Fix issue where playback controls were not kept visible on key presses ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index f92d550706d..c7ffda8ae51 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -303,6 +303,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; private int textureViewRotation; + private boolean isTouching; public PlayerView(Context context) { this(context, null); @@ -1048,11 +1049,21 @@ public SubtitleView getSubtitleView() { } @Override - public boolean onTouchEvent(MotionEvent ev) { - if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { - return false; + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + isTouching = true; + return true; + case MotionEvent.ACTION_UP: + if (isTouching) { + isTouching = false; + performClick(); + return true; + } + return false; + default: + return false; } - return performClick(); } @Override From 92e2581e238e1d5996e45e150b9326a0969e61b7 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 29 May 2019 20:42:25 +0100 Subject: [PATCH 106/219] Fix CacheUtil.cache() use too much data cache() opens all connections with unset length to avoid position errors. This makes more data then needed to be downloading by the underlying network stack. This fix makes makes it open connections for only required length. Issue:#5927 PiperOrigin-RevId: 250546175 --- RELEASENOTES.md | 15 ++++- .../upstream/cache/CacheDataSource.java | 22 ++----- .../exoplayer2/upstream/cache/CacheUtil.java | 63 +++++++++++++------ .../exoplayer2/testutil/CacheAsserts.java | 16 +++-- 4 files changed, 69 insertions(+), 47 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90c3874cd7f..49fa49ba771 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,17 +10,26 @@ `PlayerControlView`. * Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). +<<<<<<< HEAD * Fix issue where playback controls were not kept visible on key presses ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +======= +* Add a workaround for broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* Offline: + * Add Scheduler implementation which uses WorkManager. + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix CacheUtil.cache() use too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). +>>>>>>> 42ba6abf5... Fix CacheUtil.cache() use too much data * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). -* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the - preparation of the `DownloadHelper` failed - ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). * Fix bug caused by parallel adaptive track selection using `Format`s without diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 58b2d176cf2..e5df8d55c38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -134,9 +134,9 @@ public interface EventListener { private @Nullable DataSource currentDataSource; private boolean currentDataSpecLengthUnset; - private @Nullable Uri uri; - private @Nullable Uri actualUri; - private @HttpMethod int httpMethod; + @Nullable private Uri uri; + @Nullable private Uri actualUri; + @HttpMethod private int httpMethod; private int flags; private @Nullable String key; private long readPosition; @@ -319,7 +319,7 @@ public int read(byte[] buffer, int offset, int readLength) throws IOException { } return bytesRead; } catch (IOException e) { - if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { + if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) { setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } @@ -484,20 +484,6 @@ private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaul return redirectedUri != null ? redirectedUri : defaultUri; } - private static boolean isCausedByPositionOutOfRange(IOException e) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - return true; - } - } - cause = cause.getCause(); - } - return false; - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 219d736835e..9c80becdebc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -20,6 +20,7 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; @@ -195,37 +196,42 @@ public static void cache( long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; } + boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { throwExceptionIfInterruptedOrCancelled(isCanceled); long blockLength = - cache.getCachedLength( - key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); + cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft); if (blockLength > 0) { // Skip already cached data. } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; + long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength; + boolean isLastBlock = length == bytesLeft; long read = readAndDiscard( dataSpec, position, - blockLength, + length, dataSource, buffer, priorityTaskManager, priority, progressNotifier, + isLastBlock, isCanceled); if (read < blockLength) { // Reached to the end of the data. - if (enableEOFException && bytesLeft != C.LENGTH_UNSET) { + if (enableEOFException && !lengthUnset) { throw new EOFException(); } break; } } position += blockLength; - bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; + if (!lengthUnset) { + bytesLeft -= blockLength; + } } } @@ -242,6 +248,7 @@ public static void cache( * caching. * @param priority The priority of this task. * @param progressNotifier A notifier through which to report progress updates, or {@code null}. + * @param isLastBlock Whether this read block is the last block of the content. * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. @@ -255,6 +262,7 @@ private static long readAndDiscard( PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -263,22 +271,23 @@ private static long readAndDiscard( // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } + throwExceptionIfInterruptedOrCancelled(isCanceled); try { - throwExceptionIfInterruptedOrCancelled(isCanceled); - // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in - // case the given length exceeds the end of input. - dataSpec = - new DataSpec( - dataSpec.uri, - dataSpec.httpMethod, - dataSpec.httpBody, - absoluteStreamPosition, - /* position= */ dataSpec.position + positionOffset, - C.LENGTH_UNSET, - dataSpec.key, - dataSpec.flags); - long resolvedLength = dataSource.open(dataSpec); - if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + long resolvedLength; + try { + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); + } catch (IOException exception) { + if (length == C.LENGTH_UNSET + || !isLastBlock + || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); + // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent + // getting an error in case the given length exceeds the end of input. + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); + } + if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } long totalBytesRead = 0; @@ -340,6 +349,20 @@ public static void remove(Cache cache, String key) { } } + /*package*/ static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); + } + return false; + } + private static String buildCacheKey( DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) { return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY) diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 664532d3ff6..e095c559394 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -83,7 +83,8 @@ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... * @throws IOException If an error occurred reading from the Cache. */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - DataSpec dataSpec = new DataSpec(uri); + // TODO Make tests specify if the content length is stored in cache metadata. + DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); assertDataCached(cache, dataSpec, expected); } @@ -95,15 +96,18 @@ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throw public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected) throws IOException { DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); - dataSource.open(dataSpec); + byte[] bytes; try { - byte[] bytes = TestUtil.readToEnd(dataSource); - assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") - .that(bytes) - .isEqualTo(expected); + dataSource.open(dataSpec); + bytes = TestUtil.readToEnd(dataSource); + } catch (IOException e) { + throw new IOException("Opening/reading cache failed: " + dataSpec, e); } finally { dataSource.close(); } + assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") + .that(bytes) + .isEqualTo(expected); } /** From bbf8a9ac13861b16afd065f8accdd78ea826467a Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:40:00 +0100 Subject: [PATCH 107/219] Simplify CacheUtil PiperOrigin-RevId: 250654697 --- .../exoplayer2/upstream/cache/CacheUtil.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 9c80becdebc..5b066b79301 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -79,13 +79,7 @@ public static Pair getCached( DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long requestLength; - if (dataSpec.length != C.LENGTH_UNSET) { - requestLength = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } + long requestLength = getRequestLength(dataSpec, cache, key); long bytesAlreadyCached = 0; long bytesLeft = requestLength; while (bytesLeft != 0) { @@ -180,22 +174,19 @@ public static void cache( Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); + String key = buildCacheKey(dataSpec, cacheKeyFactory); + long bytesLeft; ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier(progressListener); Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); + bytesLeft = lengthAndBytesAlreadyCached.first; + } else { + bytesLeft = getRequestLength(dataSpec, cache, key); } - String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long bytesLeft; - if (dataSpec.length != C.LENGTH_UNSET) { - bytesLeft = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { throwExceptionIfInterruptedOrCancelled(isCanceled); @@ -235,6 +226,17 @@ public static void cache( } } + private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) { + if (dataSpec.length != C.LENGTH_UNSET) { + return dataSpec.length; + } else { + long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); + return contentLength == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : contentLength - dataSpec.absoluteStreamPosition; + } + } + /** * Reads and discards all data specified by the {@code dataSpec}. * From 811cdf06ac932a5ba232978f4c5c5ff9522da1d3 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:47:28 +0100 Subject: [PATCH 108/219] Modify DashDownloaderTest to test if content length is stored PiperOrigin-RevId: 250655481 --- .../dash/offline/DashDownloaderTest.java | 11 ++- .../dash/offline/DownloadManagerDashTest.java | 7 +- .../source/hls/offline/HlsDownloaderTest.java | 25 ++--- .../exoplayer2/testutil/CacheAsserts.java | 98 ++++++++++++------- 4 files changed, 88 insertions(+), 53 deletions(-) diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index b3a6b8271bf..94dae35ed55 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; @@ -108,7 +109,7 @@ public void testDownloadRepresentation() throws Exception { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -127,7 +128,7 @@ public void testDownloadRepresentationInSmallParts() throws Exception { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -146,7 +147,7 @@ public void testDownloadRepresentations() throws Exception { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -167,7 +168,7 @@ public void testDownloadAllRepresentations() throws Exception { DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -256,7 +257,7 @@ public void testDownloadRepresentationFailure() throws Exception { // Expected. } dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 35db882e2a3..280bc45b707 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -154,7 +155,7 @@ public void testSaveAndLoadActionFile() throws Throwable { public void testHandleDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -162,7 +163,7 @@ public void testHandleMultipleDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -176,7 +177,7 @@ public void testHandleInterferingDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 7d77a78316e..d06d047f669 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.DummyDataSource; @@ -129,12 +130,13 @@ public void testDownloadRepresentation() throws Exception { assertCachedData( cache, - fakeDataSet, - MASTER_PLAYLIST_URI, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MASTER_PLAYLIST_URI, + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test @@ -186,11 +188,12 @@ public void testDownloadMediaPlaylist() throws Exception { assertCachedData( cache, - fakeDataSet, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index e095c559394..00c9e60bd51 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -33,59 +33,89 @@ /** Assertion methods for {@link Cache}. */ public final class CacheAsserts { - /** - * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { - ArrayList allData = fakeDataSet.getAllData(); - Uri[] uris = new Uri[allData.size()]; - for (int i = 0; i < allData.size(); i++) { - uris[i] = allData.get(i).uri; + /** Defines a set of data requests. */ + public static final class RequestSet { + + private final FakeDataSet fakeDataSet; + private DataSpec[] dataSpecs; + + public RequestSet(FakeDataSet fakeDataSet) { + this.fakeDataSet = fakeDataSet; + ArrayList allData = fakeDataSet.getAllData(); + dataSpecs = new DataSpec[allData.size()]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(allData.get(i).uri); + } } - assertCachedData(cache, fakeDataSet, uris); - } - /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) - throws IOException { - Uri[] uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); + public RequestSet subset(String... uriStrings) { + dataSpecs = new DataSpec[uriStrings.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i])); + } + return this; + } + + public RequestSet subset(Uri... uris) { + dataSpecs = new DataSpec[uris.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(uris[i]); + } + return this; + } + + public RequestSet subset(DataSpec... dataSpecs) { + this.dataSpecs = dataSpecs; + return this; + } + + public int getCount() { + return dataSpecs.length; + } + + public byte[] getData(int i) { + return fakeDataSet.getData(dataSpecs[i].uri).getData(); + } + + public DataSpec getDataSpec(int i) { + return dataSpecs[i]; + } + + public RequestSet useBoundedDataSpecFor(String uriString) { + FakeData data = fakeDataSet.getData(uriString); + for (int i = 0; i < dataSpecs.length; i++) { + DataSpec spec = dataSpecs[i]; + if (spec.uri.getPath().equals(uriString)) { + dataSpecs[i] = spec.subrange(0, data.getData().length); + return this; + } + } + throw new IllegalStateException(); } - assertCachedData(cache, fakeDataSet, uris); } /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * Asserts that the cache contains necessary data for the {@code requestSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) - throws IOException { + public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException { int totalLength = 0; - for (Uri uri : uris) { - byte[] data = fakeDataSet.getData(uri).getData(); - assertDataCached(cache, uri, data); + for (int i = 0; i < requestSet.getCount(); i++) { + byte[] data = requestSet.getData(i); + assertDataCached(cache, requestSet.getDataSpec(i), data); totalLength += data.length; } assertThat(cache.getCacheSpace()).isEqualTo(totalLength); } /** - * Asserts that the cache contains the given data for {@code uriString}. + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - // TODO Make tests specify if the content length is stored in cache metadata. - DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); - assertDataCached(cache, dataSpec, expected); + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + assertCachedData(cache, new RequestSet(fakeDataSet)); } /** From c231e1120eb980f8ca9c658ff9336faf6db3ce23 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 14:41:03 +0100 Subject: [PATCH 109/219] Fix misreporting cached bytes when caching is paused When caching is resumed, it starts from the initial position. This makes more data to be reported as cached. Issue:#5573 PiperOrigin-RevId: 250678841 --- RELEASENOTES.md | 8 +--- .../exoplayer2/upstream/cache/CacheUtil.java | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 49fa49ba771..80aa49f3f43 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,20 +10,16 @@ `PlayerControlView`. * Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). -<<<<<<< HEAD * Fix issue where playback controls were not kept visible on key presses ([#5963](https://github.com/google/ExoPlayer/issues/5963)). -======= -* Add a workaround for broken raw audio decoding on Oppo R9 - ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: - * Add Scheduler implementation which uses WorkManager. * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Fix CacheUtil.cache() use too much data ([#5927](https://github.com/google/ExoPlayer/issues/5927)). ->>>>>>> 42ba6abf5... Fix CacheUtil.cache() use too much data + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 5b066b79301..47470c5de75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -268,6 +268,8 @@ private static long readAndDiscard( AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; + long initialPositionOffset = positionOffset; + long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET; while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. @@ -275,45 +277,51 @@ private static long readAndDiscard( } throwExceptionIfInterruptedOrCancelled(isCanceled); try { - long resolvedLength; - try { - resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); - } catch (IOException exception) { - if (length == C.LENGTH_UNSET - || !isLastBlock - || !isCausedByPositionOutOfRange(exception)) { - throw exception; + long resolvedLength = C.LENGTH_UNSET; + boolean isDataSourceOpen = false; + if (endOffset != C.POSITION_UNSET) { + // If a specific length is given, first try to open the data source for that length to + // avoid more data then required to be requested. If the given length exceeds the end of + // input we will get a "position out of range" error. In that case try to open the source + // again with unset length. + try { + resolvedLength = + dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset)); + isDataSourceOpen = true; + } catch (IOException exception) { + if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); } - Util.closeQuietly(dataSource); - // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent - // getting an error in case the given length exceeds the end of input. + } + if (!isDataSourceOpen) { resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); } if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } - long totalBytesRead = 0; - while (totalBytesRead != length) { + while (positionOffset != endOffset) { throwExceptionIfInterruptedOrCancelled(isCanceled); int bytesRead = dataSource.read( buffer, 0, - length != C.LENGTH_UNSET - ? (int) Math.min(buffer.length, length - totalBytesRead) + endOffset != C.POSITION_UNSET + ? (int) Math.min(buffer.length, endOffset - positionOffset) : buffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { if (progressNotifier != null) { - progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead); + progressNotifier.onRequestLengthResolved(positionOffset); } break; } - totalBytesRead += bytesRead; + positionOffset += bytesRead; if (progressNotifier != null) { progressNotifier.onBytesCached(bytesRead); } } - return totalBytesRead; + return positionOffset - initialPositionOffset; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { From 19de134aa659beaf6ad3255f39d0a1d3f675e56b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 3 Jun 2019 16:34:41 +0100 Subject: [PATCH 110/219] CEA608: Handling XDS and TEXT modes --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 70 +++++++++++++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80aa49f3f43..3ab6c7bd7ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,8 @@ ### 2.10.2 ### * Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). * TTML: Fix bitmap rendering ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * UI: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 9316e4fb866..774b94a43cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -80,6 +80,11 @@ public final class Cea608Decoder extends CeaDecoder { * at which point the non-displayed memory becomes the displayed memory (and vice versa). */ private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20; + + private static final byte CTRL_BACKSPACE = 0x21; + + private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; + /** * Command initiating roll-up style captioning, with the maximum of 2 rows displayed * simultaneously. @@ -95,25 +100,31 @@ public final class Cea608Decoder extends CeaDecoder { * simultaneously. */ private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27; + /** * Command initiating paint-on style captioning. Subsequent data should be addressed immediately * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command. */ private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29; /** - * Command indicating the end of a pop-on style caption. At this point the caption loaded in - * non-displayed memory should be swapped with the one in displayed memory. If no - * {@link #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the - * receiver into pop-on style. + * TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out + * until a command is received that switches back to the CAPTION service. */ - private static final byte CTRL_END_OF_CAPTION = 0x2F; + private static final byte CTRL_TEXT_RESTART = 0x2A; + + private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B; private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; - private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; - private static final byte CTRL_BACKSPACE = 0x21; + /** + * Command indicating the end of a pop-on style caption. At this point the caption loaded in + * non-displayed memory should be swapped with the one in displayed memory. If no {@link + * #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into + * pop-on style. + */ + private static final byte CTRL_END_OF_CAPTION = 0x2F; // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). private static final int[] BASIC_CHARACTER_SET = new int[] { @@ -237,6 +248,11 @@ public final class Cea608Decoder extends CeaDecoder { private byte repeatableControlCc2; private int currentChannel; + // The incoming characters may belong to 3 different services based on the last received control + // codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning + // service bytes and drops the rest. + private boolean isInCaptionService; + public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); cueBuilders = new ArrayList<>(); @@ -268,6 +284,7 @@ public Cea608Decoder(String mimeType, int accessibilityChannel) { setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); + isInCaptionService = true; } @Override @@ -288,6 +305,7 @@ public void flush() { repeatableControlCc1 = 0; repeatableControlCc2 = 0; currentChannel = NTSC_CC_CHANNEL_1; + isInCaptionService = true; } @Override @@ -363,6 +381,12 @@ protected void decode(SubtitleInputBuffer inputBuffer) { continue; } + maybeUpdateIsInCaptionService(ccData1, ccData2); + if (!isInCaptionService) { + // Only the Captioning service is supported. Drop all other bytes. + continue; + } + // Special North American character set. // ccData1 - 0|0|0|1|C|0|0|1 // ccData2 - 0|0|1|1|X|X|X|X @@ -629,6 +653,29 @@ private void resetCueBuilders() { cueBuilders.add(currentCueBuilder); } + private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) { + if (isXdsControlCode(cc1)) { + isInCaptionService = false; + } else if (isServiceSwitchCommand(cc1)) { + switch (cc2) { + case CTRL_TEXT_RESTART: + case CTRL_RESUME_TEXT_DISPLAY: + isInCaptionService = false; + break; + case CTRL_END_OF_CAPTION: + case CTRL_RESUME_CAPTION_LOADING: + case CTRL_RESUME_DIRECT_CAPTIONING: + case CTRL_ROLL_UP_CAPTIONS_2_ROWS: + case CTRL_ROLL_UP_CAPTIONS_3_ROWS: + case CTRL_ROLL_UP_CAPTIONS_4_ROWS: + isInCaptionService = true; + break; + default: + // No update. + } + } + } + private static char getChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; @@ -683,6 +730,15 @@ private static boolean isRepeatable(byte cc1) { return (cc1 & 0xF0) == 0x10; } + private static boolean isXdsControlCode(byte cc1) { + return 0x01 <= cc1 && cc1 <= 0x0F; + } + + private static boolean isServiceSwitchCommand(byte cc1) { + // cc1 - 0|0|0|1|C|1|0|0 + return (cc1 & 0xF7) == 0x14; + } + private static class CueBuilder { // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 From d11778dbc800fbb171c5de2eedd18a726797301d Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 May 2019 13:50:28 +0100 Subject: [PATCH 111/219] Add ResolvingDataSource for just-in-time resolution of DataSpecs. Issue:#5779 PiperOrigin-RevId: 249234058 --- RELEASENOTES.md | 2 + .../upstream/ResolvingDataSource.java | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3ab6c7bd7ae..17df5def0cb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.10.2 ### +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Subtitles: * CEA-608: Handle XDS and TEXT modes ([#5807](https://github.com/google/ExoPlayer/pull/5807)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java new file mode 100644 index 00000000000..99f0dee2072 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 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.net.Uri; +import androidx.annotation.Nullable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */ +public final class ResolvingDataSource implements DataSource { + + /** Resolves {@link DataSpec DataSpecs}. */ + public interface Resolver { + + /** + * Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This + * method is allowed to block until the {@link DataSpec} has been resolved. + * + *

    Note that this method is called for every new connection, so caching of results is + * recommended, especially if network operations are involved. + * + * @param dataSpec The original {@link DataSpec}. + * @return The resolved {@link DataSpec}. + * @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}. + */ + DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException; + + /** + * Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching + * purposes. + * + *

    Implementations do not need to overwrite this method unless they want to change the + * reported URI. + * + *

    This method is not allowed to block. + * + * @param uri The URI as reported by {@link DataSource#getUri()}. + * @return The resolved URI used for event reporting and caching. + */ + default Uri resolveReportedUri(Uri uri) { + return uri; + } + } + + /** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */ + public static final class Factory implements DataSource.Factory { + + private final DataSource.Factory upstreamFactory; + private final Resolver resolver; + + /** + * Creates factory for {@link ResolvingDataSource} instances. + * + * @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link + * DataSpec DataSpecs}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public Factory(DataSource.Factory upstreamFactory, Resolver resolver) { + this.upstreamFactory = upstreamFactory; + this.resolver = resolver; + } + + @Override + public DataSource createDataSource() { + return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); + } + } + + private final DataSource upstreamDataSource; + private final Resolver resolver; + + private boolean upstreamOpened; + + /** + * @param upstreamDataSource The wrapped {@link DataSource}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) { + this.upstreamDataSource = upstreamDataSource; + this.resolver = resolver; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + upstreamDataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec); + upstreamOpened = true; + return upstreamDataSource.open(resolvedDataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return upstreamDataSource.read(buffer, offset, readLength); + } + + @Nullable + @Override + public Uri getUri() { + Uri reportedUri = upstreamDataSource.getUri(); + return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri); + } + + @Override + public Map> getResponseHeaders() { + return upstreamDataSource.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + if (upstreamOpened) { + upstreamOpened = false; + upstreamDataSource.close(); + } + } +} From 578abccf1658c92b7c91b909075a8fc3297f2c60 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 17 May 2019 18:34:07 +0100 Subject: [PATCH 112/219] Add SilenceMediaSource Issue: #5735 PiperOrigin-RevId: 248745617 --- RELEASENOTES.md | 2 + .../exoplayer2/source/SilenceMediaSource.java | 242 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 17df5def0cb..6780ea97e62 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). * Subtitles: * CEA-608: Handle XDS and TEXT modes ([#5807](https://github.com/google/ExoPlayer/pull/5807)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java new file mode 100644 index 00000000000..b03dd0ea7c8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2019 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 androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Media source with a single period consisting of silent raw audio of a given duration. */ +public final class SilenceMediaSource extends BaseMediaSource { + + private static final int SAMPLE_RATE_HZ = 44100; + @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; + private static final int CHANNEL_COUNT = 2; + private static final Format FORMAT = + Format.createAudioSampleFormat( + /* id=*/ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + CHANNEL_COUNT, + SAMPLE_RATE_HZ, + ENCODING, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + private static final byte[] SILENCE_SAMPLE = + new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; + + private final long durationUs; + + /** + * Creates a new media source providing silent audio of the given duration. + * + * @param durationUs The duration of silent audio to output, in microseconds. + */ + public SilenceMediaSource(long durationUs) { + Assertions.checkArgument(durationUs >= 0); + this.durationUs = durationUs; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + refreshSourceInfo( + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), + /* manifest= */ null); + } + + @Override + public void maybeThrowSourceInfoRefreshError() {} + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return new SilenceMediaPeriod(durationUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) {} + + @Override + public void releaseSourceInternal() {} + + private static final class SilenceMediaPeriod implements MediaPeriod { + + private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT)); + + private final long durationUs; + private final ArrayList sampleStreams; + + public SilenceMediaPeriod(long durationUs) { + this.durationUs = durationUs; + sampleStreams = new ArrayList<>(); + } + + @Override + public void prepare(Callback callback, long positionUs) { + callback.onPrepared(/* mediaPeriod= */ this); + } + + @Override + public void maybeThrowPrepareError() {} + + @Override + public TrackGroupArray getTrackGroups() { + return TRACKS; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + sampleStreams.remove(streams[i]); + streams[i] = null; + } + if (streams[i] == null && selections[i] != null) { + SilenceSampleStream stream = new SilenceSampleStream(durationUs); + stream.seekTo(positionUs); + sampleStreams.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; + } + } + return positionUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) {} + + @Override + public long readDiscontinuity() { + return C.TIME_UNSET; + } + + @Override + public long seekToUs(long positionUs) { + for (int i = 0; i < sampleStreams.size(); i++) { + ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); + } + return positionUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + + @Override + public long getBufferedPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public long getNextLoadPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public boolean continueLoading(long positionUs) { + return false; + } + + @Override + public void reevaluateBuffer(long positionUs) {} + } + + private static final class SilenceSampleStream implements SampleStream { + + private final long durationBytes; + + private boolean sentFormat; + private long positionBytes; + + public SilenceSampleStream(long durationUs) { + durationBytes = getAudioByteCount(durationUs); + seekTo(0); + } + + public void seekTo(long positionUs) { + positionBytes = getAudioByteCount(positionUs); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() {} + + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { + if (!sentFormat || formatRequired) { + formatHolder.format = FORMAT; + sentFormat = true; + return C.RESULT_FORMAT_READ; + } + + long bytesRemaining = durationBytes - positionBytes; + if (bytesRemaining == 0) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining); + buffer.ensureSpaceForWrite(bytesToWrite); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite); + buffer.timeUs = getAudioPositionUs(positionBytes); + positionBytes += bytesToWrite; + return C.RESULT_BUFFER_READ; + } + + @Override + public int skipData(long positionUs) { + long oldPositionBytes = positionBytes; + seekTo(positionUs); + return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length); + } + } + + private static long getAudioByteCount(long durationUs) { + long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND; + return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount; + } + + private static long getAudioPositionUs(long bytes) { + long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT); + return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ; + } +} From edee3dd3409710abf8d3c7a6301ca1be62f8a5a2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 14:11:16 +0100 Subject: [PATCH 113/219] Bump to 2.10.2 PiperOrigin-RevId: 251216822 --- RELEASENOTES.md | 28 +++++++++---------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6780ea97e62..e7be123c8b2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,18 +6,6 @@ ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Add `SilenceMediaSource` that can be used to play silence of a given duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). -* Subtitles: - * CEA-608: Handle XDS and TEXT modes - ([#5807](https://github.com/google/ExoPlayer/pull/5807)). - * TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). -* UI: - * Allow setting `DefaultTimeBar` attributes on `PlayerView` and - `PlayerControlView`. - * Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). - * Fix issue where playback controls were not kept visible on key presses - ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Offline: * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed @@ -26,11 +14,23 @@ ([#5927](https://github.com/google/ExoPlayer/issues/5927)). * Fix misreporting cached bytes when caching is paused ([#5573](https://github.com/google/ExoPlayer/issues/5573)). -* Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). -* Add ProgressUpdateListener to PlayerControlView +* Add `ProgressUpdateListener` to `PlayerControlView` ([#5834](https://github.com/google/ExoPlayer/issues/5834)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). diff --git a/constants.gradle b/constants.gradle index b2ee322ee69..bf464ad2c11 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.1' - releaseVersionCode = 2010001 + releaseVersion = '2.10.2' + releaseVersionCode = 2010002 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 a90435227bb..db3f3943e10 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.1"; + public static final String VERSION = "2.10.2"; /** 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.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.2"; /** * 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 = 2010001; + public static final int VERSION_INT = 2010002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 83a6d51fd12371758e79a1f46e078f33b4a2c065 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 4 Jun 2019 10:19:29 +0100 Subject: [PATCH 114/219] Use listener notification batching in CastPlayer PiperOrigin-RevId: 251399230 --- .../exoplayer2/ext/cast/CastPlayer.java | 107 +++++++++++++----- 1 file changed, 76 insertions(+), 31 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 14bb433d2bd..0cf31c1a469 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CopyOnWriteArrayList; /** * {@link Player} implementation that communicates with a Cast receiver app. @@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer { private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; - // Listeners. - private final CopyOnWriteArraySet listeners; + // Listeners and notification. + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. @@ -113,7 +118,9 @@ public CastPlayer(CastContext castContext) { period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); - listeners = new CopyOnWriteArraySet<>(); + listeners = new CopyOnWriteArrayList<>(); + notificationsBatch = new ArrayList<>(); + ongoingNotificationsTasks = new ArrayDeque<>(); SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); @@ -296,12 +303,17 @@ public Looper getApplicationLooper() { @Override public void addListener(EventListener listener) { - listeners.add(listener); + listeners.addIfAbsent(new ListenerHolder(listener)); } @Override public void removeListener(EventListener listener) { - listeners.remove(listener); + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); + } + } } @Override @@ -347,14 +359,13 @@ public void seekTo(int windowIndex, long positionMs) { pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK))); } else if (pendingSeekCount == 0) { - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); } + flushNotifications(); } @Override @@ -530,30 +541,31 @@ public void updateInternalState() { || this.playWhenReady != playWhenReady) { this.playbackState = playbackState; this.playWhenReady = playWhenReady; - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(this.playWhenReady, this.playbackState); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState))); } @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); if (this.repeatMode != repeatMode) { this.repeatMode = repeatMode; - for (EventListener listener : listeners) { - listener.onRepeatModeChanged(repeatMode); - } + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION))); } if (updateTracksAndSelections()) { - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } maybeUpdateTimelineAndNotify(); + flushNotifications(); } private void maybeUpdateTimelineAndNotify() { @@ -561,9 +573,10 @@ private void maybeUpdateTimelineAndNotify() { @Player.TimelineChangeReason int reason = waitingForInitialTimeline ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; waitingForInitialTimeline = false; - for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null, reason); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason))); } } @@ -826,7 +839,23 @@ public void onSessionResuming(CastSession castSession, String s) { } - // Result callbacks hooks. + // Internal methods. + + private void flushNotifications() { + boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty(); + ongoingNotificationsTasks.addAll(notificationsBatch); + notificationsBatch.clear(); + if (recursiveNotification) { + // This will be handled once the current notification task is finished. + return; + } + while (!ongoingNotificationsTasks.isEmpty()) { + ongoingNotificationsTasks.peekFirst().execute(); + ongoingNotificationsTasks.removeFirst(); + } + } + + // Internal classes. private final class SeekResultCallback implements ResultCallback { @@ -840,9 +869,25 @@ public void onResult(@NonNull MediaChannelResult result) { if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); + flushNotifications(); + } + } + } + + private final class ListenerNotificationTask { + + private final Iterator listenersSnapshot; + private final ListenerInvocation listenerInvocation; + + private ListenerNotificationTask(ListenerInvocation listenerInvocation) { + this.listenersSnapshot = listeners.iterator(); + this.listenerInvocation = listenerInvocation; + } + + public void execute() { + while (listenersSnapshot.hasNext()) { + listenersSnapshot.next().invoke(listenerInvocation); } } } From 2f8c8b609f6d526c8404088a9b7602d726001b0e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:14:14 +0100 Subject: [PATCH 115/219] Fix detection of current window index in CastPlayer Issue:#5955 PiperOrigin-RevId: 251616118 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/cast/CastPlayer.java | 23 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e7be123c8b2..22c61066d15 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ * Fix bug caused by parallel adaptive track selection using `Format`s without bitrate information ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). ### 2.10.1 ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 0cf31c1a469..4b973715b1f 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -551,7 +551,17 @@ public void updateInternalState() { notificationsBatch.add( new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } - int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); + maybeUpdateTimelineAndNotify(); + + int currentWindowIndex = C.INDEX_UNSET; + MediaQueueItem currentItem = remoteMediaClient.getCurrentItem(); + if (currentItem != null) { + currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId()); + } + if (currentWindowIndex == C.INDEX_UNSET) { + // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do. + currentWindowIndex = 0; + } if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; notificationsBatch.add( @@ -564,7 +574,6 @@ public void updateInternalState() { new ListenerNotificationTask( listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } - maybeUpdateTimelineAndNotify(); flushNotifications(); } @@ -714,16 +723,6 @@ private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) { } } - /** - * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If - * there is no media session, returns 0. - */ - private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { - Integer currentItemId = mediaStatus != null - ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; - return currentItemId != null ? currentItemId : 0; - } - private static boolean isTrackActive(long id, long[] activeTrackIds) { for (long activeTrackId : activeTrackIds) { if (activeTrackId == id) { From f638634fe2c7eab3b8a4334ee07a9d321ba9a921 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:28:37 +0100 Subject: [PATCH 116/219] Simplify re-creation of the CastPlayer queue in the Cast demo app PiperOrigin-RevId: 251617354 --- .../exoplayer2/castdemo/DefaultReceiverPlayerManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index 4b71b3a001c..df153a14232 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -66,7 +66,6 @@ private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -268,9 +267,6 @@ public void onPositionDiscontinuity(@DiscontinuityReason int reason) { public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { updateCurrentItemIndex(); - if (currentPlayer == castPlayer && timeline.isEmpty()) { - castMediaQueueCreationPending = true; - } } // CastPlayer.SessionAvailabilityListener implementation. @@ -332,7 +328,6 @@ private void setCurrentPlayer(Player currentPlayer) { this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -352,12 +347,11 @@ private void setCurrentPlayer(Player currentPlayer) { */ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { maybeSetCurrentItemAndNotify(itemIndex); - if (castMediaQueueCreationPending) { + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { items[i] = buildMediaQueueItem(mediaQueue.get(i)); } - castMediaQueueCreationPending = false; castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { currentPlayer.seekTo(itemIndex, positionMs); From d3967b557a044f2cdbed77e70a0ecfd0c13c0457 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 00:42:40 +0100 Subject: [PATCH 117/219] Don't throw DecoderQueryException from getCodecMaxSize It's only thrown in an edge case on API level 20 and below. If it is thrown it causes playback failure when playback could succeed, by throwing up through configureCodec. It seems better just to catch the exception and have the codec be configured using the format's own width and height. PiperOrigin-RevId: 251745539 --- .../mediacodec/MediaCodecRenderer.java | 4 +-- .../video/MediaCodecVideoRenderer.java | 35 ++++++++++--------- .../testutil/DebugRenderersFactory.java | 4 +-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index be08186dc0e..5f7f5d60b7f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -453,15 +453,13 @@ protected abstract List getDecoderInfos( * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected abstract void configureCodec( MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException; + float codecOperatingRate); protected final void maybeInitCodec() throws ExoPlaybackException { if (codec != null || inputFormat == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 193fbddfec1..e75a3866b6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -550,8 +550,7 @@ protected void configureCodec( MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( @@ -1173,11 +1172,9 @@ protected MediaFormat getMediaFormat( * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected CodecMaxValues getCodecMaxValues( - MediaCodecInfo codecInfo, Format format, Format[] streamFormats) - throws DecoderQueryException { + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(codecInfo, format); @@ -1227,17 +1224,15 @@ protected CodecMaxValues getCodecMaxValues( } /** - * Returns a maximum video size to use when configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats that are expected to have the - * same aspect ratio, but whose sizes are unknown. + * Returns a maximum video size to use when configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats that are expected to have the same + * aspect ratio, but whose sizes are unknown. * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format for which the codec is being configured. * @return The maximum video size to use, or null if the size of {@code format} should be used. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) - throws DecoderQueryException { + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { boolean isVerticalVideo = format.height > format.width; int formatLongEdgePx = isVerticalVideo ? format.height : format.width; int formatShortEdgePx = isVerticalVideo ? format.width : format.height; @@ -1255,12 +1250,18 @@ private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) return alignedSize; } } else { - // Conservatively assume the codec requires 16px width and height alignment. - longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; - shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; - if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { - return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, - isVerticalVideo ? longEdgePx : shortEdgePx); + try { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point( + isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } catch (DecoderQueryException e) { + // We tried our best. Give up! + return null; } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 92ec23c34d2..9feaf6863a0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.nio.ByteBuffer; @@ -114,8 +113,7 @@ protected void configureCodec( MediaCodec codec, Format format, MediaCrypto crypto, - float operatingRate) - throws DecoderQueryException { + float operatingRate) { // If the codec is being initialized whilst the renderer is started, default behavior is to // render the first frame (i.e. the keyframe before the current position), then drop frames up // to the current playback position. For test runs that place a maximum limit on the number of From 95c08ad8642cc5c85aaa2b2bd80c2181adc2dcee Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 18 Jun 2019 19:41:01 +0100 Subject: [PATCH 118/219] tell user that #1234 should be the issue number --- .github/ISSUE_TEMPLATE/bug.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index a4996278bd1..c0980df4401 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -36,16 +36,17 @@ or a small sample app that you’re able to share as source code on GitHub. Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to media that reproduces the issue. If you don't wish to post it publicly, please submit the issue, then email the link to dev.exoplayer@gmail.com using a subject -in the format "Issue #1234". Provide all the metadata we'd need to play the -content like drm license urls or similar. If the content is accessible only in -certain countries or regions, please say so. +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. ### [REQUIRED] A full bug report captured from the device Capture a full bug report using "adb bugreport". Output from "adb logcat" or a log snippet is NOT sufficient. Please attach the captured bug report as a file. If you don't wish to post it publicly, please submit the issue, then email the bug report to dev.exoplayer@gmail.com using a subject in the format -"Issue #1234". +"Issue #1234", where "#1234" should be replaced with your issue number. ### [REQUIRED] Version of ExoPlayer being used Specify the absolute version number. Avoid using terms such as "latest". From 67879f9557d2165e964a6d1ea1cd434d39ed8575 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 18 Jun 2019 19:42:39 +0100 Subject: [PATCH 119/219] add sections asking for bug report --- .github/ISSUE_TEMPLATE/content_not_playing.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index ff29f3a7d1c..c8d4668a6ad 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -33,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems. Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to media that reproduces the issue. If you don't wish to post it publicly, please submit the issue, then email the link to dev.exoplayer@gmail.com using a subject -in the format "Issue #1234". Provide all the metadata we'd need to play the -content like drm license urls or similar. If the content is accessible only in -certain countries or regions, please say so. +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. ### [REQUIRED] Version of ExoPlayer being used Specify the absolute version number. Avoid using terms such as "latest". @@ -44,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest". Specify the devices and versions of Android on which you expect the content to play. If possible, please test on multiple devices and Android versions. +### [REQUIRED] A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234", where "#1234" should be replaced with your issue number. + + + diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java new file mode 100644 index 00000000000..01801c9897e --- /dev/null +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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.ext.workmanager; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; + +/** A {@link Scheduler} that uses {@link WorkManager}. */ +public final class WorkManagerScheduler implements Scheduler { + + private static final boolean DEBUG = false; + private static final String TAG = "WorkManagerScheduler"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; + + private final String workName; + + /** + * @param workName A name for work scheduled by this instance. If the same name was used by a + * previous instance, anything scheduled by the previous instance will be canceled by this + * instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are + * called. + */ + public WorkManagerScheduler(String workName) { + this.workName = workName; + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + Constraints constraints = buildConstraints(requirements); + Data inputData = buildInputData(requirements, servicePackage, serviceAction); + OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData); + logd("Scheduling work: " + workName); + WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest); + return true; + } + + @Override + public boolean cancel() { + logd("Canceling work: " + workName); + WorkManager.getInstance().cancelUniqueWork(workName); + return true; + } + + private static Constraints buildConstraints(Requirements requirements) { + Constraints.Builder builder = new Constraints.Builder(); + + if (requirements.isUnmeteredNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.UNMETERED); + } else if (requirements.isNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED); + } + + if (requirements.isChargingRequired()) { + builder.setRequiresCharging(true); + } + + if (requirements.isIdleRequired() && Util.SDK_INT >= 23) { + setRequiresDeviceIdle(builder); + } + + return builder.build(); + } + + @TargetApi(23) + private static void setRequiresDeviceIdle(Constraints.Builder builder) { + builder.setRequiresDeviceIdle(true); + } + + private static Data buildInputData( + Requirements requirements, String servicePackage, String serviceAction) { + Data.Builder builder = new Data.Builder(); + + builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements()); + builder.putString(KEY_SERVICE_PACKAGE, servicePackage); + builder.putString(KEY_SERVICE_ACTION, serviceAction); + + return builder.build(); + } + + private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) { + OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class); + + builder.setConstraints(constraints); + builder.setInputData(inputData); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link Worker} that starts the target service if the requirements are met. */ + // This class needs to be public so that WorkManager can instantiate it. + public static final class SchedulerWorker extends Worker { + + private final WorkerParameters workerParams; + private final Context context; + + public SchedulerWorker(Context context, WorkerParameters workerParams) { + super(context, workerParams); + this.workerParams = workerParams; + this.context = context; + } + + @Override + public Result doWork() { + logd("SchedulerWorker is started"); + Data inputData = workerParams.getInputData(); + Assertions.checkNotNull(inputData, "Work started without input data."); + Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0)); + if (requirements.checkRequirements(context)) { + logd("Requirements are met"); + String serviceAction = inputData.getString(KEY_SERVICE_ACTION); + String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE); + Assertions.checkNotNull(serviceAction, "Service action missing."); + Assertions.checkNotNull(servicePackage, "Service package missing."); + Intent intent = new Intent(serviceAction).setPackage(servicePackage); + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(context, intent); + return Result.success(); + } else { + logd("Requirements are not met"); + return Result.retry(); + } + } + } +} From 67ad84f121767da42b185aa475daa9105724aa66 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 19:15:08 +0100 Subject: [PATCH 164/219] Remove more classes from nullness blacklist PiperOrigin-RevId: 256202135 --- .../ext/cast/DefaultCastOptionsProvider.java | 3 +- .../exoplayer2/ext/flac/FlacDecoder.java | 2 + .../exoplayer2/ext/flac/FlacDecoderJni.java | 42 +++++++++++-------- extensions/opus/build.gradle | 1 + .../exoplayer2/ext/opus/OpusDecoder.java | 2 + .../exoplayer2/ext/opus/OpusLibrary.java | 6 +-- .../exoplayer2/ext/vp9/VpxDecoder.java | 6 ++- .../exoplayer2/ext/vp9/VpxLibrary.java | 7 ++-- .../exoplayer2/decoder/SimpleDecoder.java | 3 +- 9 files changed, 45 insertions(+), 27 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 06f0bec971a..4ce45a92b12 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -20,6 +20,7 @@ import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.SessionProvider; +import java.util.Collections; import java.util.List; /** @@ -36,7 +37,7 @@ public CastOptions getCastOptions(Context context) { @Override public List getAdditionalSessionProviders(Context context) { - return null; + return Collections.emptyList(); } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 2d74bce5f16..9b15aff846d 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; @@ -94,6 +95,7 @@ protected FlacDecoderException createUnexpectedDecodeException(Throwable error) } @Override + @Nullable protected FlacDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index de038921aa5..a97d99fa541 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -37,15 +39,16 @@ public FlacFrameDecodeException(String message, int errorCode) { } } - private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac. private final long nativeDecoderContext; - private ByteBuffer byteBufferData; - private ExtractorInput extractorInput; + @Nullable private ByteBuffer byteBufferData; + @Nullable private ExtractorInput extractorInput; + @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - private byte[] tempBuffer; + @SuppressWarnings("nullness:method.invocation.invalid") public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -58,7 +61,8 @@ public FlacDecoderJni() throws FlacDecoderException { /** * Sets data to be parsed by libflac. - * @param byteBufferData Source {@link ByteBuffer} + * + * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; @@ -68,7 +72,8 @@ public void setData(ByteBuffer byteBufferData) { /** * Sets data to be parsed by libflac. - * @param extractorInput Source {@link ExtractorInput} + * + * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; @@ -90,15 +95,15 @@ public boolean isEndOfData() { /** * Reads up to {@code length} bytes from the data source. - *

    - * This method blocks until at least one byte of data can be read, the end of the input is + * + *

    This method blocks until at least one byte of data can be read, the end of the input is * detected or an exception is thrown. - *

    - * This method is called from the native code. + * + *

    This method is called from the native code. * * @param target A target {@link ByteBuffer} into which data should be written. - * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns - * zero; it just means all the data read from the source. + * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been + * read from the source, then 0 is returned. */ public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); @@ -106,18 +111,20 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { byteCount = Math.min(byteCount, byteBufferData.remaining()); int originalLimit = byteBufferData.limit(); byteBufferData.limit(byteBufferData.position() + byteCount); - target.put(byteBufferData); - byteBufferData.limit(originalLimit); } else if (extractorInput != null) { + ExtractorInput extractorInput = this.extractorInput; + byte[] tempBuffer = Util.castNonNull(this.tempBuffer); byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); - int read = readFromExtractorInput(0, byteCount); + int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount); if (read < 4) { // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // the buffer of the input. Do another read to reduce the number of calls to this method // from the native code. - read += readFromExtractorInput(read, byteCount - read); + read += + readFromExtractorInput( + extractorInput, tempBuffer, read, /* length= */ byteCount - read); } byteCount = read; target.put(tempBuffer, 0, byteCount); @@ -234,7 +241,8 @@ public void release() { flacRelease(nativeDecoderContext); } - private int readFromExtractorInput(int offset, int length) + private int readFromExtractorInput( + ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException, InterruptedException { int read = extractorInput.read(tempBuffer, offset, length); if (read == C.RESULT_END_OF_INPUT) { diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 56acbdb7d36..0795079c6b1 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,6 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index f8ec477b880..dbce33b923c 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -150,6 +151,7 @@ protected OpusDecoderException createUnexpectedDecodeException(Throwable error) } @Override + @Nullable protected OpusDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 285be96388b..2c2c8f69724 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; @@ -49,9 +50,8 @@ public static boolean isAvailable() { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? opusGetVersion() : null; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 57e5481b557..0e13e826300 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; @@ -120,8 +121,9 @@ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { } @Override - protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, - boolean reset) { + @Nullable + protected VpxDecoderException decode( + VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 5a65fc56ff9..db056d51109 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; @@ -49,9 +50,8 @@ public static boolean isAvailable() { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? vpxGetVersion() : null; } @@ -60,6 +60,7 @@ public static String getVersion() { * Returns the configuration string with which the underlying library was built if available, or * null otherwise. */ + @Nullable public static String getBuildConfig() { return isAvailable() ? vpxGetBuildConfig() : null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index f8204f6be32..b5650860e9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -301,5 +301,6 @@ private void releaseOutputBufferInternal(O outputBuffer) { * @param reset Whether the decoder must be reset before decoding. * @return A decoder exception if an error occurred, or null if decoding was successful. */ - protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset); + @Nullable + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); } From 98714235ad93caee62f586c6fa66f3fe3b9b572a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:23:52 +0100 Subject: [PATCH 165/219] Simplify FlacExtractor (step toward enabling nullness checking) - Inline some unnecessarily split out helper methods - Clear ExtractorInput from FlacDecoderJni data after usage - Clean up exception handling for StreamInfo decode failures PiperOrigin-RevId: 256524955 --- .../ext/flac/FlacBinarySearchSeekerTest.java | 4 +- .../ext/flac/FlacExtractorTest.java | 2 +- .../exoplayer2/ext/flac/FlacDecoder.java | 8 +- .../exoplayer2/ext/flac/FlacDecoderJni.java | 34 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 182 ++++++++---------- 5 files changed, 113 insertions(+), 117 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 435279fc452..934d7cf1063 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -52,7 +52,7 @@ public void testGetSeekMap_returnsSeekMapWithCorrectDuration() FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +70,7 @@ public void testSetSeekTargetUs_returnsSeekPending() decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); assertThat(seeker.isSeeking()).isTrue(); diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index d9cbac6ad5c..97f152cea47 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -28,7 +28,7 @@ public class FlacExtractorTest { @Before - public void setUp() throws Exception { + public void setUp() { if (!FlacLibrary.isAvailable()) { fail("Flac library not available."); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 9b15aff846d..d20c18e9571 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -17,6 +17,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -59,14 +60,13 @@ public FlacDecoder( decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); FlacStreamInfo streamInfo; try { - streamInfo = decoderJni.decodeMetadata(); + streamInfo = decoderJni.decodeStreamInfo(); + } catch (ParserException e) { + throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { // Never happens. throw new IllegalStateException(e); } - if (streamInfo == null) { - throw new FlacDecoderException("Metadata decoding failed"); - } int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index a97d99fa541..32ef22dab06 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -17,6 +17,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; @@ -48,7 +49,6 @@ public FlacFrameDecodeException(String message, int errorCode) { @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - @SuppressWarnings("nullness:method.invocation.invalid") public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -60,37 +60,46 @@ public FlacDecoderJni() throws FlacDecoderException { } /** - * Sets data to be parsed by libflac. + * Sets the data to be parsed. * * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; this.extractorInput = null; - this.tempBuffer = null; } /** - * Sets data to be parsed by libflac. + * Sets the data to be parsed. * * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; this.extractorInput = extractorInput; + endOfExtractorInput = false; if (tempBuffer == null) { - this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; + tempBuffer = new byte[TEMP_BUFFER_SIZE]; } - endOfExtractorInput = false; } + /** + * Returns whether the end of the data to be parsed has been reached, or true if no data was set. + */ public boolean isEndOfData() { if (byteBufferData != null) { return byteBufferData.remaining() == 0; } else if (extractorInput != null) { return endOfExtractorInput; + } else { + return true; } - return true; + } + + /** Clears the data to be parsed. */ + public void clearData() { + byteBufferData = null; + extractorInput = null; } /** @@ -99,12 +108,11 @@ public boolean isEndOfData() { *

    This method blocks until at least one byte of data can be read, the end of the input is * detected or an exception is thrown. * - *

    This method is called from the native code. - * * @param target A target {@link ByteBuffer} into which data should be written. * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been * read from the source, then 0 is returned. */ + @SuppressWarnings("unused") // Called from native code. public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); if (byteBufferData != null) { @@ -135,8 +143,12 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { } /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { - return flacDecodeMetadata(nativeDecoderContext); + public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { + FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); + if (streamInfo == null) { + throw new ParserException("Failed to decode StreamInfo"); + } + return streamInfo; } /** diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index bb72e114fe0..491b9621296 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -21,7 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -75,22 +75,19 @@ public final class FlacExtractor implements Extractor { private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private final Id3Peeker id3Peeker; - private final boolean isId3MetadataDisabled; + private final boolean id3MetadataDisabled; - private FlacDecoderJni decoderJni; + @Nullable private FlacDecoderJni decoderJni; + @Nullable private ExtractorOutput extractorOutput; + @Nullable private TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private boolean streamInfoDecoded; + @Nullable private FlacStreamInfo streamInfo; + @Nullable private ParsableByteArray outputBuffer; + @Nullable private OutputFrameHolder outputFrameHolder; - private ParsableByteArray outputBuffer; - private ByteBuffer outputByteBuffer; - private BinarySearchSeeker.OutputFrameHolder outputFrameHolder; - private FlacStreamInfo streamInfo; - - private Metadata id3Metadata; - private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; - - private boolean readPastStreamInfo; + @Nullable private Metadata id3Metadata; + @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -104,7 +101,7 @@ public FlacExtractor() { */ public FlacExtractor(int flags) { id3Peeker = new Id3Peeker(); - isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @Override @@ -130,48 +127,53 @@ public boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { + if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { id3Metadata = peekId3Data(input); } decoderJni.setData(input); - readPastStreamInfo(input); + try { + decodeStreamInfo(input); - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); - } + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return handlePendingSeek(input, seekPosition); + } - long lastDecodePosition = decoderJni.getDecodePosition(); - try { - decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); - } catch (FlacDecoderJni.FlacFrameDecodeException e) { - throw new IOException("Cannot read frame at position " + lastDecodePosition, e); - } - int outputSize = outputByteBuffer.limit(); - if (outputSize == 0) { - return RESULT_END_OF_INPUT; - } + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + long lastDecodePosition = decoderJni.getDecodePosition(); + try { + decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + throw new IOException("Cannot read frame at position " + lastDecodePosition, e); + } + int outputSize = outputByteBuffer.limit(); + if (outputSize == 0) { + return RESULT_END_OF_INPUT; + } - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp()); + return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } finally { + decoderJni.clearData(); + } } @Override public void seek(long position, long timeUs) { if (position == 0) { - readPastStreamInfo = false; + streamInfoDecoded = false; } if (decoderJni != null) { decoderJni.reset(position); } - if (flacBinarySearchSeeker != null) { - flacBinarySearchSeeker.setSeekTargetUs(timeUs); + if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); } } @Override public void release() { - flacBinarySearchSeeker = null; + binarySearchSeeker = null; if (decoderJni != null) { decoderJni.release(); decoderJni = null; @@ -179,16 +181,15 @@ public void release() { } /** - * Peeks ID3 tag data (if present) at the beginning of the input. + * Peeks ID3 tag data at the beginning of the input. * - * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not - * present in the input. + * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input. */ @Nullable private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { input.resetPeekPosition(); Id3Decoder.FramePredicate id3FramePredicate = - isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; + id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; return id3Peeker.peekId3Data(input, id3FramePredicate); } @@ -199,68 +200,61 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt */ private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, 0, FLAC_SIGNATURE.length); + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); return Arrays.equals(header, FLAC_SIGNATURE); } - private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { - if (readPastStreamInfo) { + private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { + if (streamInfoDecoded) { return; } - FlacStreamInfo streamInfo = decodeStreamInfo(input); - readPastStreamInfo = true; - if (this.streamInfo == null) { - updateFlacStreamInfo(input, streamInfo); - } - } - - private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; - outputSeekMap(input, streamInfo); - outputFormat(streamInfo); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); - outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); - outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer); - } - - private FlacStreamInfo decodeStreamInfo(ExtractorInput input) - throws InterruptedException, IOException { + FlacStreamInfo streamInfo; try { - FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - return streamInfo; + streamInfo = decoderJni.decodeStreamInfo(); } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); + decoderJni.reset(/* newPosition= */ 0); + input.setRetryPosition(/* position= */ 0, e); throw e; } + + streamInfoDecoded = true; + if (this.streamInfo == null) { + this.streamInfo = streamInfo; + outputSeekMap(streamInfo, input.getLength()); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); + } } - private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { - boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; - SeekMap seekMap = - hasSeekTable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : getSeekMapForNonSeekTableFlac(input, streamInfo); - extractorOutput.seekMap(seekMap); + private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + throws InterruptedException, IOException { + int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs); + } + return seekResult; } - private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET) { + private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + SeekMap seekMap; + if (hasSeekTable) { + seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + } else if (inputLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); - flacBinarySearchSeeker = + binarySearchSeeker = new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); - return flacBinarySearchSeeker.getSeekMap(); - } else { // can't seek at all, because there's no SeekTable and the input length is unknown. - return new SeekMap.Unseekable(streamInfo.durationUs()); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); } + extractorOutput.seekMap(seekMap); } - private void outputFormat(FlacStreamInfo streamInfo) { + private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -277,25 +271,15 @@ private void outputFormat(FlacStreamInfo streamInfo) { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); + metadata); trackOutput.format(mediaFormat); } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) - throws InterruptedException, IOException { - int seekResult = - flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); - ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; - if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs); - } - return seekResult; - } - - private void writeLastSampleToOutput(int size, long lastSampleTimestamp) { - outputBuffer.setPosition(0); - trackOutput.sampleData(outputBuffer, size); - trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + private void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + sampleData.setPosition(0); + trackOutput.sampleData(sampleData, size); + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ From 008efd10a4ba4aff4c68be7be5c46601e79373c1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:32:20 +0100 Subject: [PATCH 166/219] Make FlacExtractor output methods static This gives a caller greater confidence that the methods have no side effects, and remove any nullness issues with these methods accessing @Nullable member variables. PiperOrigin-RevId: 256525739 --- .../exoplayer2/ext/flac/FlacExtractor.java | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 491b9621296..b50554e2f6c 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -151,7 +152,7 @@ public int read(final ExtractorInput input, PositionHolder seekPosition) return RESULT_END_OF_INPUT; } - outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp()); + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } finally { decoderJni.clearData(); @@ -193,17 +194,6 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt return id3Peeker.peekId3Data(input, id3FramePredicate); } - /** - * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. - * - * @return Whether the input begins with {@link #FLAC_SIGNATURE}. - */ - private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); - } - private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -221,8 +211,9 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, streamInfoDecoded = true; if (this.streamInfo == null) { this.streamInfo = streamInfo; - outputSeekMap(streamInfo, input.getLength()); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + binarySearchSeeker = + outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -230,31 +221,56 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) throws InterruptedException, IOException { + Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs); + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); } return seekResult; } - private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + /** + * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. + * + * @return Whether the input begins with {@link #FLAC_SIGNATURE}. + */ + private static boolean peekFlacSignature(ExtractorInput input) + throws IOException, InterruptedException { + byte[] header = new byte[FLAC_SIGNATURE.length]; + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); + return Arrays.equals(header, FLAC_SIGNATURE); + } + + /** + * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to + * handle seeks. + */ + @Nullable + private static FlacBinarySearchSeeker outputSeekMap( + FlacDecoderJni decoderJni, + FlacStreamInfo streamInfo, + long streamLength, + ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); - } else if (inputLength != C.LENGTH_UNSET) { + } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); } - extractorOutput.seekMap(seekMap); + output.seekMap(seekMap); + return binarySearchSeeker; } - private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { + private static void outputFormat( + FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -272,13 +288,14 @@ private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { /* selectionFlags= */ 0, /* language= */ null, metadata); - trackOutput.format(mediaFormat); + output.format(mediaFormat); } - private void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + private static void outputSample( + ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) { sampleData.setPosition(0); - trackOutput.sampleData(sampleData, size); - trackOutput.sampleMetadata( + output.sampleData(sampleData, size); + output.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } From a2a14146231a803ecc5bd3a0afde0cfa0e842d7b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:38:48 +0100 Subject: [PATCH 167/219] Remove FlacExtractor from nullness blacklist PiperOrigin-RevId: 256526365 --- extensions/flac/build.gradle | 1 + .../exoplayer2/ext/flac/FlacExtractor.java | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 06a58884046..10b244cb39a 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -40,6 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index b50554e2f6c..082068f34df 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -43,6 +43,9 @@ import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Facilitates the extraction of data from the FLAC container format. @@ -75,17 +78,17 @@ public final class FlacExtractor implements Extractor { */ private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; + private final ParsableByteArray outputBuffer; private final Id3Peeker id3Peeker; private final boolean id3MetadataDisabled; @Nullable private FlacDecoderJni decoderJni; - @Nullable private ExtractorOutput extractorOutput; - @Nullable private TrackOutput trackOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; private boolean streamInfoDecoded; - @Nullable private FlacStreamInfo streamInfo; - @Nullable private ParsableByteArray outputBuffer; - @Nullable private OutputFrameHolder outputFrameHolder; + private @MonotonicNonNull FlacStreamInfo streamInfo; + private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; @@ -101,6 +104,7 @@ public FlacExtractor() { * @param flags Flags that control the extractor's behavior. */ public FlacExtractor(int flags) { + outputBuffer = new ParsableByteArray(); id3Peeker = new Id3Peeker(); id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @@ -132,12 +136,12 @@ public int read(final ExtractorInput input, PositionHolder seekPosition) id3Metadata = peekId3Data(input); } - decoderJni.setData(input); + FlacDecoderJni decoderJni = initDecoderJni(input); try { decodeStreamInfo(input); if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); + return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); } ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; @@ -194,6 +198,17 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt return id3Peeker.peekId3Data(input, id3FramePredicate); } + @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) + private FlacDecoderJni initDecoderJni(ExtractorInput input) { + FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni); + decoderJni.setData(input); + return decoderJni; + } + + @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. + @EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -214,14 +229,19 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + @RequiresNonNull("binarySearchSeeker") + private int handlePendingSeek( + ExtractorInput input, + PositionHolder seekPosition, + ParsableByteArray outputBuffer, + OutputFrameHolder outputFrameHolder, + TrackOutput trackOutput) throws InterruptedException, IOException { - Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { @@ -270,7 +290,7 @@ private static FlacBinarySearchSeeker outputSeekMap( } private static void outputFormat( - FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { + FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, From 383c0adcca44a907699b3873489a17563ad5f064 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 20:02:20 +0100 Subject: [PATCH 168/219] Remove more low hanging fruit from nullness blacklist PiperOrigin-RevId: 256573352 --- .../extractor/flv/ScriptTagPayloadReader.java | 22 ++++++++++++++----- .../exoplayer2/extractor/mp4/Track.java | 1 + .../extractor/mp4/TrackEncryptionBox.java | 2 +- .../google/android/exoplayer2/text/Cue.java | 7 +++--- .../text/SimpleSubtitleDecoder.java | 2 ++ .../exoplayer2/text/SubtitleOutputBuffer.java | 9 ++++---- .../exoplayer2/text/pgs/PgsDecoder.java | 5 ++++- .../exoplayer2/text/ssa/SsaDecoder.java | 7 +++--- .../exoplayer2/text/ssa/SsaSubtitle.java | 4 ++-- .../exoplayer2/text/subrip/SubripDecoder.java | 6 +++-- .../text/subrip/SubripSubtitle.java | 4 ++-- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 16 +++++++++----- 12 files changed, 57 insertions(+), 28 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index eb1cc8f336d..806cc9fad44 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,7 +46,7 @@ private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -138,7 +140,10 @@ private static ArrayList readAmfStrictArray(ParsableByteArray data) { ArrayList list = new ArrayList<>(count); for (int i = 0; i < count; i++) { int type = readAmfType(data); - list.add(readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } } return list; } @@ -157,7 +162,10 @@ private static HashMap readAmfObject(ParsableByteArray data) { if (type == AMF_TYPE_END_MARKER) { break; } - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -174,7 +182,10 @@ private static HashMap readAmfEcmaArray(ParsableByteArray data) for (int i = 0; i < count; i++) { String key = readAmfString(data); int type = readAmfType(data); - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -191,6 +202,7 @@ private static Date readAmfDate(ParsableByteArray data) { return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,8 +220,8 @@ private static Object readAmfData(ParsableByteArray data, int type) { case AMF_TYPE_DATE: return readAmfDate(data); default: + // We don't log a warning because there are types that we knowingly don't support. return null; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 9d3635e8b39..7676926c4dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -123,6 +123,7 @@ public Track(int id, int type, long timescale, long movieTimescale, long duratio * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no * such entry exists. */ + @Nullable public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { return sampleDescriptionEncryptionBoxes == null ? null : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index 5bd29c6e756..a35d211aa46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -52,7 +52,7 @@ public final class TrackEncryptionBox { * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the * track encryption box or sample group description box. Null otherwise. */ - public final byte[] defaultInitializationVector; + @Nullable public final byte[] defaultInitializationVector; /** * @param isEncrypted See {@link #isEncrypted}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 4b54b3ea9aa..3f6ff44248f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -28,9 +28,10 @@ */ public class Cue { - /** - * An unset position or width. - */ + /** The empty cue. */ + public static final Cue EMPTY = new Cue(""); + + /** An unset position or width. */ public static final float DIMEN_UNSET = Float.MIN_VALUE; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index 38d6ff25cb7..bd561afaf8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -69,6 +70,7 @@ protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) { @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 75b7a016738..843cfab045b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -45,22 +46,22 @@ public void setContent(long timeUs, Subtitle subtitle, long subsampleOffsetUs) { @Override public int getEventTimeCount() { - return subtitle.getEventTimeCount(); + return Assertions.checkNotNull(subtitle).getEventTimeCount(); } @Override public long getEventTime(int index) { - return subtitle.getEventTime(index) + subsampleOffsetUs; + return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs; } @Override public int getNextEventTimeIndex(long timeUs) { - return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs); } @Override public List getCues(long timeUs) { - return subtitle.getCues(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 091bda49f32..9ef3556c8fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.pgs; import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; - private Inflater inflater; + @Nullable private Inflater inflater; public PgsDecoder() { super("PgsDecoder"); @@ -76,6 +77,7 @@ private void maybeInflateData(ParsableByteArray buffer) { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) bitmapY = buffer.readUnsignedShort(); } + @Nullable public Cue build() { if (planeWidth == 0 || planeHeight == 0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c25b26128c5..b1af75f6137 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.ssa; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -49,7 +50,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatTextIndex; public SsaDecoder() { - this(null); + this(/* initializationData= */ null); } /** @@ -58,7 +59,7 @@ public SsaDecoder() { * format line. The second must contain an SSA header that will be assumed common to all * samples. */ - public SsaDecoder(List initializationData) { + public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; @@ -201,7 +202,7 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu cues.add(new Cue(text)); cueTimesUs.add(startTimeUs); if (endTimeUs != C.TIME_UNSET) { - cues.add(null); + cues.add(Cue.EMPTY); cueTimesUs.add(endTimeUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 339119ed6b5..9a3756194f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -32,7 +32,7 @@ private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ public long getEventTime(int index) { @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 6f9fd366ec7..5dfaecee1d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -111,11 +111,13 @@ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { // Read and parse the text and tags. textBuilder.setLength(0); tags.clear(); - while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + currentLine = subripData.readLine(); + while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
    "); } textBuilder.append(processLine(currentLine, tags)); + currentLine = subripData.readLine(); } Spanned text = Html.fromHtml(textBuilder.toString()); @@ -132,7 +134,7 @@ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { cues.add(buildCue(text, alignmentTag)); if (haveEndTimecode) { - cues.add(null); + cues.add(Cue.EMPTY); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java index a79df478e57..01ed1711a9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java @@ -32,7 +32,7 @@ private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ public long getEventTime(int index) { @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 9211dc51ce3..ddc7a8f5f82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; private int defaultFontFace; private int defaultColorRgba; @@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); - decodeInitializationData(initializationData); - } - private void decodeInitializationData(List initializationData) { if (initializationData != null && initializationData.size() == 1 && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { byte[] initializationBytes = initializationData.get(0); @@ -151,8 +149,16 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) } parsableByteArray.setPosition(position + atomSize); } - return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + return new Tx3gSubtitle( + new Cue( + cueText, + /* textAlignment= */ null, + verticalPlacement, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET)); } private static String readSubtitleText(ParsableByteArray parsableByteArray) From b5e3ae454249af6b5932145d2a417452d113f53a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Jul 2019 17:07:38 +0100 Subject: [PATCH 169/219] Add Nullable annotations to CastPlayer PiperOrigin-RevId: 256680382 --- .../exoplayer2/ext/cast/CastPlayer.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 4b973715b1f..bc0987322be 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -83,8 +83,6 @@ public final class CastPlayer extends BasePlayer { private final CastTimelineTracker timelineTracker; private final Timeline.Period period; - private RemoteMediaClient remoteMediaClient; - // Result callbacks. private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; @@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer { private final CopyOnWriteArrayList listeners; private final ArrayList notificationsBatch; private final ArrayDeque ongoingNotificationsTasks; - private SessionAvailabilityListener sessionAvailabilityListener; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. + @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; @@ -148,6 +147,7 @@ public CastPlayer(CastContext castContext) { * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ + @Nullable public PendingResult loadItem(MediaQueueItem item, long positionMs) { return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } @@ -163,8 +163,9 @@ public PendingResult loadItem(MediaQueueItem item, long posi * @param repeatMode The repeat mode for the created media queue. * @return The Cast {@code PendingResult}, or null if no session is available. */ - public PendingResult loadItems(MediaQueueItem[] items, int startIndex, - long positionMs, @RepeatMode int repeatMode) { + @Nullable + public PendingResult loadItems( + MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; @@ -180,6 +181,7 @@ public PendingResult loadItems(MediaQueueItem[] items, int s * @param items The items to append. * @return The Cast {@code PendingResult}, or null if no media queue exists. */ + @Nullable public PendingResult addItems(MediaQueueItem... items) { return addItems(MediaQueueItem.INVALID_ITEM_ID, items); } @@ -194,6 +196,7 @@ public PendingResult addItems(MediaQueueItem... items) { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult addItems(int periodId, MediaQueueItem... items) { if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { @@ -211,6 +214,7 @@ public PendingResult addItems(int periodId, MediaQueueItem.. * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult removeItem(int periodId) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { return remoteMediaClient.queueRemoveItem(periodId, null); @@ -229,6 +233,7 @@ public PendingResult removeItem(int periodId) { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult moveItem(int periodId, int newIndex) { Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { @@ -246,6 +251,7 @@ public PendingResult moveItem(int periodId, int newIndex) { * @return The item that corresponds to the period with the given id, or null if no media queue or * period with id {@code periodId} exist. */ + @Nullable public MediaQueueItem getItem(int periodId) { MediaStatus mediaStatus = getMediaStatus(); return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET @@ -264,9 +270,9 @@ public boolean isCastSessionAvailable() { /** * Sets a listener for updates on the cast session availability. * - * @param listener The {@link SessionAvailabilityListener}. + * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener. */ - public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { + public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { sessionAvailabilityListener = listener; } @@ -322,6 +328,7 @@ public int getPlaybackState() { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -529,7 +536,7 @@ public long getContentBufferedPosition() { // Internal methods. - public void updateInternalState() { + private void updateInternalState() { if (remoteMediaClient == null) { // There is no session. We leave the state of the player as it is now. return; @@ -675,7 +682,8 @@ private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) } } - private @Nullable MediaStatus getMediaStatus() { + @Nullable + private MediaStatus getMediaStatus() { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } From b6777e030e7cec89ef7b735515be8c2d0ef5bb82 Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 6 Jul 2019 11:09:36 +0100 Subject: [PATCH 170/219] Remove some UI classes from nullness blacklist PiperOrigin-RevId: 256751627 --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 14 ++++++++------ .../android/exoplayer2/ui/PlayerControlView.java | 11 +++++++---- .../android/exoplayer2/ui/SubtitleView.java | 15 ++++++++++----- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d4a37ea4ef6..268219b6d5d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -18,6 +18,7 @@ import android.content.Context; import android.content.res.TypedArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import java.lang.annotation.Documented; @@ -97,16 +98,16 @@ void onAspectRatioUpdated( private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; - private AspectRatioListener aspectRatioListener; + @Nullable private AspectRatioListener aspectRatioListener; private float videoAspectRatio; - private @ResizeMode int resizeMode; + @ResizeMode private int resizeMode; public AspectRatioFrameLayout(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); resizeMode = RESIZE_MODE_FIT; if (attrs != null) { @@ -136,9 +137,10 @@ public void setAspectRatio(float widthHeightRatio) { /** * Sets the {@link AspectRatioListener}. * - * @param listener The listener to be notified about aspect ratios changes. + * @param listener The listener to be notified about aspect ratios changes, or null to clear a + * listener that was previously set. */ - public void setAspectRatioListener(AspectRatioListener listener) { + public void setAspectRatioListener(@Nullable AspectRatioListener listener) { this.aspectRatioListener = listener; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 383d7966925..73bb98a1a07 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -281,19 +281,22 @@ public interface ProgressUpdateListener { private long currentWindowOffset; public PlayerControlView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerControlView(Context context, AttributeSet attrs) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, attrs); } public PlayerControlView( - Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; rewindMs = DEFAULT_REWIND_MS; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 5d99eda1096..0bdc1acc885 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -53,8 +53,8 @@ public final class SubtitleView extends View implements TextOutput { private final List painters; - private List cues; - private @Cue.TextSizeType int textSizeType; + @Nullable private List cues; + @Cue.TextSizeType private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -62,10 +62,10 @@ public final class SubtitleView extends View implements TextOutput { private float bottomPaddingFraction; public SubtitleView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public SubtitleView(Context context, AttributeSet attrs) { + public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; @@ -246,7 +246,11 @@ public void setBottomPaddingFraction(float bottomPaddingFraction) { @Override public void dispatchDraw(Canvas canvas) { - int cueCount = (cues == null) ? 0 : cues.size(); + List cues = this.cues; + if (cues == null || cues.isEmpty()) { + return; + } + int rawViewHeight = getHeight(); // Calculate the cue box bounds relative to the canvas after padding is taken into account. @@ -267,6 +271,7 @@ public void dispatchDraw(Canvas canvas) { return; } + int cueCount = cues.size(); for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); From bba0a27cb6fda742e29ef31aa5b52889076fb181 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 14 Jul 2019 16:24:00 +0100 Subject: [PATCH 171/219] Merge pull request #6151 from ittiam-systems:bug-5527 PiperOrigin-RevId: 257668797 --- RELEASENOTES.md | 2 + extensions/flac/proguard-rules.txt | 2 +- .../ext/flac/FlacBinarySearchSeekerTest.java | 10 +- .../ext/flac/FlacBinarySearchSeeker.java | 22 ++--- .../exoplayer2/ext/flac/FlacDecoder.java | 10 +- .../exoplayer2/ext/flac/FlacDecoderJni.java | 16 +-- .../exoplayer2/ext/flac/FlacExtractor.java | 56 ++++++----- extensions/flac/src/main/jni/flac_jni.cc | 44 +++++++-- extensions/flac/src/main/jni/flac_parser.cc | 22 +++++ .../flac/src/main/jni/include/flac_parser.h | 14 +++ .../exoplayer2/extractor/ogg/FlacReader.java | 28 ++++-- .../metadata/vorbis/VorbisComment.java | 99 +++++++++++++++++++ ...treamInfo.java => FlacStreamMetadata.java} | 60 ++++++++--- .../metadata/vorbis/VorbisCommentTest.java | 42 ++++++++ .../exoplayer2/util/ColorParserTest.java | 2 +- .../util/FlacStreamMetadataTest.java | 83 ++++++++++++++++ 16 files changed, 424 insertions(+), 88 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java rename library/core/src/main/java/com/google/android/exoplayer2/util/{FlacStreamInfo.java => FlacStreamMetadata.java} (68%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 33cab068192..03298229d68 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,8 @@ ### 2.10.4 ### * Offline: Add Scheduler implementation which uses WorkManager. +* Flac extension: Parse `VORBIS_COMMENT` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index ee0a9fa5b53..b44dab34455 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -9,6 +9,6 @@ -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } --keep class com.google.android.exoplayer2.util.FlacStreamInfo { +-keep class com.google.android.exoplayer2.util.FlacStreamMetadata { *; } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 934d7cf1063..a3770afc788 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -52,7 +52,10 @@ public void testGetSeekMap_returnsSeekMapWithCorrectDuration() FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), + /* firstFramePosition= */ 0, + data.length, + decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +73,10 @@ public void testSetSeekTargetUs_returnsSeekPending() decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), + /* firstFramePosition= */ 0, + data.length, + decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); assertThat(seeker.isSeeking()).isTrue(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index b9c6ea06dd4..4bfcc003ec9 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; @@ -34,20 +34,20 @@ private final FlacDecoderJni decoderJni; public FlacBinarySearchSeeker( - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long firstFramePosition, long inputLength, FlacDecoderJni decoderJni) { super( - new FlacSeekTimestampConverter(streamInfo), + new FlacSeekTimestampConverter(streamMetadata), new FlacTimestampSeeker(decoderJni), - streamInfo.durationUs(), + streamMetadata.durationUs(), /* floorTimePosition= */ 0, - /* ceilingTimePosition= */ streamInfo.totalSamples, + /* ceilingTimePosition= */ streamMetadata.totalSamples, /* floorBytePosition= */ firstFramePosition, /* ceilingBytePosition= */ inputLength, - /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), - /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); + /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); } @@ -112,15 +112,15 @@ public TimestampSearchResult searchForTimestamp( * the timestamp for a stream seek time position. */ private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { - private final FlacStreamInfo streamInfo; + private final FlacStreamMetadata streamMetadata; - public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; + public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) { + this.streamMetadata = streamMetadata; } @Override public long timeUsToTargetTime(long timeUs) { - return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); + return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs); } } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index d20c18e9571..50eb048d98b 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -58,9 +58,9 @@ public FlacDecoder( } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { @@ -69,9 +69,9 @@ public FlacDecoder( } int initialInputBufferSize = - maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; + maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); + maxOutputBufferSize = streamMetadata.maxDecodedFrameSize(); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 32ef22dab06..f454e28c68f 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -142,13 +142,13 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { return byteCount; } - /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { - FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); - if (streamInfo == null) { - throw new ParserException("Failed to decode StreamInfo"); + /** Decodes and consumes the metadata from the FLAC stream. */ + public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException { + FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext); + if (streamMetadata == null) { + throw new ParserException("Failed to decode stream metadata"); } - return streamInfo; + return streamMetadata; } /** @@ -266,7 +266,7 @@ private int readFromExtractorInput( private native long flacInit(); - private native FlacStreamInfo flacDecodeMetadata(long context) + private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException, InterruptedException; private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 082068f34df..151875c2c5e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -34,7 +34,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -86,8 +86,8 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull ExtractorOutput extractorOutput; private @MonotonicNonNull TrackOutput trackOutput; - private boolean streamInfoDecoded; - private @MonotonicNonNull FlacStreamInfo streamInfo; + private boolean streamMetadataDecoded; + private @MonotonicNonNull FlacStreamMetadata streamMetadata; private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @@ -138,7 +138,7 @@ public int read(final ExtractorInput input, PositionHolder seekPosition) FlacDecoderJni decoderJni = initDecoderJni(input); try { - decodeStreamInfo(input); + decodeStreamMetadata(input); if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); @@ -166,7 +166,7 @@ public int read(final ExtractorInput input, PositionHolder seekPosition) @Override public void seek(long position, long timeUs) { if (position == 0) { - streamInfoDecoded = false; + streamMetadataDecoded = false; } if (decoderJni != null) { decoderJni.reset(position); @@ -207,29 +207,33 @@ private FlacDecoderJni initDecoderJni(ExtractorInput input) { } @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. - @EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded. @SuppressWarnings({"contracts.postcondition.not.satisfied"}) - private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { - if (streamInfoDecoded) { + private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException { + if (streamMetadataDecoded) { return; } - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (IOException e) { decoderJni.reset(/* newPosition= */ 0); input.setRetryPosition(/* position= */ 0, e); throw e; } - streamInfoDecoded = true; - if (this.streamInfo == null) { - this.streamInfo = streamInfo; + streamMetadataDecoded = true; + if (this.streamMetadata == null) { + this.streamMetadata = streamMetadata; binarySearchSeeker = - outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); - outputBuffer.reset(streamInfo.maxDecodedFrameSize()); + outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); + Metadata metadata = id3MetadataDisabled ? null : id3Metadata; + if (streamMetadata.vorbisComments != null) { + metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); + } + outputFormat(streamMetadata, metadata, trackOutput); + outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } @@ -269,38 +273,38 @@ private static boolean peekFlacSignature(ExtractorInput input) @Nullable private static FlacBinarySearchSeeker outputSeekMap( FlacDecoderJni decoderJni, - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { - seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); + new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { - seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); } output.seekMap(seekMap); return binarySearchSeeker; } private static void outputFormat( - FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { + FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - streamInfo.bitRate(), - streamInfo.maxDecodedFrameSize(), - streamInfo.channels, - streamInfo.sampleRate, - getPcmEncoding(streamInfo.bitsPerSample), + streamMetadata.bitRate(), + streamMetadata.maxDecodedFrameSize(), + streamMetadata.channels, + streamMetadata.sampleRate, + getPcmEncoding(streamMetadata.bitsPerSample), /* encoderDelay= */ 0, /* encoderPadding= */ 0, /* initializationData= */ null, diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 298719d48d0..4ba071e1cae 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -14,9 +14,12 @@ * limitations under the License. */ -#include #include +#include + #include +#include + #include "include/flac_parser.h" #define LOG_TAG "flac_jni" @@ -95,19 +98,40 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { return NULL; } + jclass arrayListClass = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = + env->GetMethodID(arrayListClass, "", "()V"); + jobject commentList = env->NewObject(arrayListClass, arrayListConstructor); + + if (context->parser->isVorbisCommentsValid()) { + jmethodID arrayListAddMethod = + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + std::vector vorbisComments = + context->parser->getVorbisComments(); + for (std::vector::const_iterator vorbisComment = + vorbisComments.begin(); + vorbisComment != vorbisComments.end(); ++vorbisComment) { + jstring commentString = env->NewStringUTF((*vorbisComment).c_str()); + env->CallBooleanMethod(commentList, arrayListAddMethod, commentString); + env->DeleteLocalRef(commentString); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); - jclass cls = env->FindClass( + jclass flacStreamMetadataClass = env->FindClass( "com/google/android/exoplayer2/util/" - "FlacStreamInfo"); - jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V"); - - return env->NewObject(cls, constructor, streamInfo.min_blocksize, - streamInfo.max_blocksize, streamInfo.min_framesize, - streamInfo.max_framesize, streamInfo.sample_rate, - streamInfo.channels, streamInfo.bits_per_sample, - streamInfo.total_samples); + "FlacStreamMetadata"); + jmethodID flacStreamMetadataConstructor = env->GetMethodID( + flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;)V"); + + return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, + streamInfo.min_blocksize, streamInfo.max_blocksize, + streamInfo.min_framesize, streamInfo.max_framesize, + streamInfo.sample_rate, streamInfo.channels, + streamInfo.bits_per_sample, streamInfo.total_samples, + commentList); } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 83d3367415c..b2d074252dd 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -172,6 +172,25 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { case FLAC__METADATA_TYPE_SEEKTABLE: mSeekTable = &metadata->data.seek_table; break; + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + if (!mVorbisCommentsValid) { + FLAC__StreamMetadata_VorbisComment vorbisComment = + metadata->data.vorbis_comment; + for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry = + vorbisComment.comments[i]; + if (vorbisCommentEntry.entry != NULL) { + std::string comment( + reinterpret_cast(vorbisCommentEntry.entry), + vorbisCommentEntry.length); + mVorbisComments.push_back(comment); + } + } + mVorbisCommentsValid = true; + } else { + ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); + } + break; default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -233,6 +252,7 @@ FLACParser::FLACParser(DataSource *source) mCurrentPos(0LL), mEOF(false), mStreamInfoValid(false), + mVorbisCommentsValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -266,6 +286,8 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_STREAMINFO); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_SEEKTABLE); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_VORBIS_COMMENT); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index cea7fbe33be..d9043e95487 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -19,6 +19,10 @@ #include +#include +#include +#include + // libFLAC parser #include "FLAC/stream_decoder.h" @@ -44,6 +48,10 @@ class FLACParser { return mStreamInfo; } + bool isVorbisCommentsValid() { return mVorbisCommentsValid; } + + std::vector getVorbisComments() { return mVorbisComments; } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -71,6 +79,8 @@ class FLACParser { mEOF = false; if (newPosition == 0) { mStreamInfoValid = false; + mVorbisCommentsValid = false; + mVorbisComments.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -116,6 +126,10 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + // cached when the VORBIS_COMMENT metadata is parsed by libFLAC + std::vector mVorbisComments; + bool mVorbisCommentsValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 5eb0727908d..d4c2bbb485d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -38,7 +38,7 @@ private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamInfo streamInfo; + private FlacStreamMetadata streamMetadata; private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { @@ -50,7 +50,7 @@ public static boolean verifyBitstreamType(ParsableByteArray data) { protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ protected long preparePayload(ParsableByteArray packet) { protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { byte[] data = packet.data; - if (streamInfo == null) { - streamInfo = new FlacStreamInfo(data, 17); + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List initializationData = Collections.singletonList(metadata); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, - Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_FLAC, + null, + Format.NO_VALUE, + streamMetadata.bitRate(), + streamMetadata.channels, + streamMetadata.sampleRate, + initializationData, + null, + 0, + null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); @@ -211,7 +221,7 @@ public SeekPoints getSeekPoints(long timeUs) { @Override public long getDurationUs() { - return streamInfo.durationUs(); + return streamMetadata.durationUs(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java new file mode 100644 index 00000000000..b1951cbc13e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019 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.metadata.vorbis; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; + +/** A vorbis comment. */ +public final class VorbisComment implements Metadata.Entry { + + /** The key. */ + public final String key; + + /** The value. */ + public final String value; + + /** + * @param key The key. + * @param value The value. + */ + public VorbisComment(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisComment(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return "VC: " + key + "=" + value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + VorbisComment other = (VorbisComment) obj; + return key.equals(other.key) && value.equals(other.value); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisComment createFromParcel(Parcel in) { + return new VorbisComment(in); + } + + @Override + public VorbisComment[] newArray(int size) { + return new VorbisComment[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java similarity index 68% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 0df39e103df..43fdda367e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import java.util.ArrayList; +import java.util.List; -/** - * Holder for FLAC stream info. - */ -public final class FlacStreamInfo { +/** Holder for FLAC metadata. */ +public final class FlacStreamMetadata { + + private static final String TAG = "FlacStreamMetadata"; public final int minBlockSize; public final int maxBlockSize; @@ -30,16 +35,19 @@ public final class FlacStreamInfo { public final int channels; public final int bitsPerSample; public final long totalSamples; + @Nullable public final Metadata vorbisComments; + + private static final String SEPARATOR = "="; /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * Parses binary FLAC stream info metadata. * - * @param data An array holding FLAC stream info metadata structure - * @param offset Offset of the structure in the array + * @param data An array containing binary FLAC stream info metadata. + * @param offset The offset of the stream info metadata in {@code data}. * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo(byte[] data, int offset) { + public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); scratch.setPosition(offset * 8); this.minBlockSize = scratch.readBits(16); @@ -49,14 +57,11 @@ public FlacStreamInfo(byte[] data, int offset) { this.sampleRate = scratch.readBits(20); this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; - this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) - | (scratch.readBits(32) & 0xFFFFFFFFL); - // Remaining 16 bytes is md5 value + this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); + this.vorbisComments = null; } /** - * Constructs a FlacStreamInfo given the parameters. - * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. * @param minFrameSize Minimum frame size of the FLAC stream. @@ -65,10 +70,13 @@ public FlacStreamInfo(byte[] data, int offset) { * @param channels Number of channels of the FLAC stream. * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. + * @param vorbisComments Vorbis comments. Each entry must be in key=value form. * @see FLAC format * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_VORBIS_COMMENT */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -76,7 +84,8 @@ public FlacStreamInfo( int sampleRate, int channels, int bitsPerSample, - long totalSamples) { + long totalSamples, + List vorbisComments) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -85,6 +94,7 @@ public FlacStreamInfo( this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; + this.vorbisComments = parseVorbisComments(vorbisComments); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ @@ -126,4 +136,24 @@ public long getApproxBytesPerFrame() { } return approxBytesPerFrame; } + + @Nullable + private static Metadata parseVorbisComments(@Nullable List vorbisComments) { + if (vorbisComments == null || vorbisComments.isEmpty()) { + return null; + } + + ArrayList commentFrames = new ArrayList<>(); + for (String vorbisComment : vorbisComments) { + String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); + if (keyAndValue.length != 2) { + Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); + } else { + VorbisComment commentFrame = new VorbisComment(keyAndValue[0], keyAndValue[1]); + commentFrames.add(commentFrame); + } + } + + return commentFrames.isEmpty() ? null : new Metadata(commentFrames); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java new file mode 100644 index 00000000000..868b28b0e1e --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.metadata.vorbis; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisComment}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentTest { + + @Test + public void testParcelable() { + VorbisComment vorbisCommentFrameToParcel = new VorbisComment("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisComment vorbisCommentFrameFromParcel = VorbisComment.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index 0392f8b26dc..2a1c59e7df3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -28,7 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for ColorParser. */ +/** Unit test for {@link ColorParser}. */ @RunWith(AndroidJUnit4.class) public final class ColorParserTest { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java new file mode 100644 index 00000000000..325d9b19f68 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link FlacStreamMetadata}. */ +@RunWith(AndroidJUnit4.class) +public final class FlacStreamMetadataTest { + + @Test + public void parseVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=Song"); + commentsList.add("Artist=Singer"); + + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + + assertThat(metadata.length()).isEqualTo(2); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Song"); + commentFrame = (VorbisComment) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } + + @Test + public void parseEmptyVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + + assertThat(metadata).isNull(); + } + + @Test + public void parseVorbisCommentWithEqualsInValue() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=So=ng"); + + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("So=ng"); + } + + @Test + public void parseInvalidVorbisComment() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("TitleSong"); + commentsList.add("Artist=Singer"); + + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } +} From fa691035d3cb51debd041fa10d157b68aa8294a0 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 10:10:39 +0100 Subject: [PATCH 172/219] Extend RK video_decoder workaround to newer API levels Issue: #6184 PiperOrigin-RevId: 258527533 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index c3072a1590a..a8cf0f12e2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1806,9 +1806,8 @@ private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format form */ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { String name = codecInfo.name; - return (Util.SDK_INT <= 17 - && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name))) + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } From 962d5e7040d1aeb6e1098488851965e42f862e33 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 17 Jul 2019 17:33:36 +0100 Subject: [PATCH 173/219] Keep default start position (TIME_UNSET) as content position for preroll ads. If we use the default start position, we currently resolve it immediately even if we need to play an ad first, and later try to project forward again if we believe that the default start position should be used. This causes problems if a specific start position is set and the later projection after the preroll ad shouldn't take place. The problem is solved by keeping the content position as TIME_UNSET (= default position) if an ad needs to be played first. The content after the ad can then be resolved to its current default position if needed. PiperOrigin-RevId: 258583948 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 7 ++- .../android/exoplayer2/MediaPeriodInfo.java | 3 +- .../android/exoplayer2/MediaPeriodQueue.java | 21 ++++---- .../android/exoplayer2/PlaybackInfo.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 51 +++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 2 +- 8 files changed, 77 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 03298229d68..05e0e45ca8f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,7 @@ * Offline: Add Scheduler implementation which uses WorkManager. * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix issue where initial seek positions get ignored when playing a preroll ad. ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c0040580829..a10416fac8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -532,7 +532,9 @@ public int getCurrentAdIndexInAdGroup() { public long getContentPosition() { if (isPlayingAd()) { playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + return playbackInfo.contentPositionUs == C.TIME_UNSET + ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() + : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); } else { return getCurrentPosition(); } 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 a9fe73371aa..65a6866a9fb 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 @@ -1304,8 +1304,11 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) Pair defaultPosition = getPeriodPosition( timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); + if (!newPeriodId.isAd()) { + // Keep unset start position if we need to play an ad first. + newContentPositionUs = defaultPosition.second; + } } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // window we can restart from. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index bc1ea7b1e1c..2733df7ba6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -29,7 +29,8 @@ public final long startPositionUs; /** * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * otherwise. + * if this is not an ad or the next content media period should be played from its default + * position. */ public final long contentPositionUs; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 86fa5e11eeb..2927d031143 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -144,7 +144,9 @@ public MediaPeriod enqueueNextMediaPeriod( MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null - ? (info.id.isAd() ? info.contentPositionUs : 0) + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( @@ -560,6 +562,7 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +571,7 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { // We're starting to buffer a new window. When playback transitions to this window we'll // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; Pair defaultPosition = timeline.getPeriodPosition( window, @@ -587,12 +591,13 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { windowSequenceNumber = nextWindowSequenceNumber++; } } else { + // We're starting to buffer a new period within the same window. startPositionUs = 0; + contentPositionUs = 0; } MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo( - periodId, /* contentPositionUs= */ startPositionUs, startPositionUs); + return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -616,13 +621,11 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } else { - // Play content from the ad group position. As a special case, if we're transitioning from a - // preroll ad group to content and there are no other ad groups, project the start position - // forward as if this were a transition to a new window. No attempt is made to handle - // midrolls in live streams, as it's unclear what content position should play after an ad - // (server-side dynamic ad insertion is more appropriate for this use case). + // Play content from the ad group position. long startPositionUs = mediaPeriodInfo.contentPositionUs; - if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { + if (startPositionUs == C.TIME_UNSET) { + // If we're transitioning from an ad group to content starting from its default position, + // project the start position forward as if this were a transition to a new window. Pair defaultPosition = timeline.getPeriodPosition( window, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 0792bf0c7d8..7107963c833 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -48,7 +48,8 @@ /** * If {@link #periodId} refers to an ad, the position of the suspended content relative to the * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} - * if {@link #periodId} does not refer to an ad. + * if {@link #periodId} does not refer to an ad or if the suspended content should be played from + * its default position. */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index a715289a04f..440a84bacb8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -20,6 +20,7 @@ import android.content.Context; import android.graphics.SurfaceTexture; +import android.net.Uri; import androidx.annotation.Nullable; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; @@ -2608,6 +2609,56 @@ public void run(SimpleExoPlayer player) { assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); } + @Test + public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000, + adPlaybackState)); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + contentStartPositionMs.set(playerReference.get().getContentPosition()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .seek(5_000) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 517f1ce2e7c..2f91c1926cb 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -418,7 +418,7 @@ public ExoPlayerTestRunner start() { if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource); + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } From e181d4bd3520756a590078b6248a891e915c0977 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jul 2019 18:22:04 +0100 Subject: [PATCH 174/219] Fix DataSchemeDataSource re-opening and range requests Issue:#6192 PiperOrigin-RevId: 258592902 --- RELEASENOTES.md | 2 + .../upstream/DataSchemeDataSource.java | 30 ++++++---- .../upstream/DataSchemeDataSourceTest.java | 60 +++++++++++++++++-- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 05e0e45ca8f..17190cfc0d4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Fix issue where initial seek positions get ignored when playing a preroll ad. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index de4a75d607c..94a6e21c86a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import android.util.Base64; @@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private @Nullable DataSpec dataSpec; - private int bytesRead; - private @Nullable byte[] data; + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int endPosition; + private int readPosition; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -41,6 +44,7 @@ public DataSchemeDataSource() { public long open(DataSpec dataSpec) throws IOException { transferInitializing(dataSpec); this.dataSpec = dataSpec; + readPosition = (int) dataSpec.position; Uri uri = dataSpec.uri; String scheme = uri.getScheme(); if (!SCHEME_DATA.equals(scheme)) { @@ -61,8 +65,14 @@ public long open(DataSpec dataSpec) throws IOException { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); } + endPosition = + dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; + if (endPosition > data.length || readPosition > endPosition) { + data = null; + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } transferStarted(dataSpec); - return data.length; + return (long) endPosition - readPosition; } @Override @@ -70,29 +80,29 @@ public int read(byte[] buffer, int offset, int readLength) { if (readLength == 0) { return 0; } - int remainingBytes = data.length - bytesRead; + int remainingBytes = endPosition - readPosition; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(data, bytesRead, buffer, offset, readLength); - bytesRead += readLength; + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; bytesTransferred(readLength); return readLength; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } @Override - public void close() throws IOException { + public void close() { if (data != null) { data = null; transferEnded(); } dataSpec = null; } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 2df9a608e9d..8cb142f05da 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -21,6 +21,7 @@ import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.junit.Before; @@ -31,6 +32,9 @@ @RunWith(AndroidJUnit4.class) public final class DataSchemeDataSourceTest { + private static final String DATA_SCHEME_URI = + "data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiLCJjb250ZW50X2lkIjoiTWpBeE5WOTBaV" + + "0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiXX0="; private DataSource schemeDataDataSource; @Before @@ -40,9 +44,7 @@ public void setUp() { @Test public void testBase64Data() throws IOException { - DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" - + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" - + "DAwMDAwMDAwMDAiXX0="); + DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI); DataSourceAsserts.assertDataSourceContent( schemeDataDataSource, dataSpec, @@ -72,6 +74,52 @@ public void testPartialReads() throws IOException { assertThat(Util.fromUtf8Bytes(buffer, 0, 18)).isEqualTo("012345678901234567"); } + @Test + public void testSequentialRangeRequests() throws IOException { + DataSpec dataSpec = + buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); + } + + @Test + public void testInvalidStartPositionRequest() throws IOException { + try { + // Try to open a range starting one byte beyond the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 108, /* length= */ C.LENGTH_UNSET)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + + @Test + public void testRangeExceedingResourceLengthRequest() throws IOException { + try { + // Try to open a range exceeding the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + @Test public void testIncorrectScheme() { try { @@ -99,7 +147,11 @@ public void testMalformedData() { } private static DataSpec buildDataSpec(String uriString) { - return new DataSpec(Uri.parse(uriString)); + return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET); + } + + private static DataSpec buildDataSpec(String uriString, int position, int length) { + return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null); } } From f82920926d107252f3bceea78c8a5da215b43d47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 10:08:19 +0100 Subject: [PATCH 175/219] Switch language normalization to 2-letter language codes. 2-letter codes (ISO 639-1) are the standard Android normalization and thus we should prefer them to 3-letter codes (although both are technically allowed according the BCP47). This helps in two ways: 1. It simplifies app interaction with our normalized language codes as the Locale class makes it easy to convert a 2-letter to a 3-letter code but not the other way round. 2. It better normalizes codes on API<21 where we previously had issues with language+country codes (see tests). 3. It allows us to normalize both ISO 639-2/T and ISO 639-2/B codes to the same language. PiperOrigin-RevId: 258729728 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 10 +-- .../google/android/exoplayer2/util/Util.java | 80 ++++++++++++++++--- .../android/exoplayer2/util/UtilTest.java | 46 ++++++++--- .../playlist/HlsMasterPlaylistParserTest.java | 2 +- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 17190cfc0d4..f0813034e07 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,8 @@ * Fix issue where initial seek positions get ignored when playing a preroll ad. * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 949bd178eaa..b8dd40f8bdc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -2318,14 +2318,14 @@ protected static int getFormatLanguageScore(Format format, @Nullable String lang if (TextUtils.equals(format.language, language)) { return 3; } - // Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") if (format.language.startsWith(language) || language.startsWith(format.language)) { return 2; } - // Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca") - if (format.language.length() >= 3 - && language.length() >= 3 - && format.language.substring(0, 3).equals(language.substring(0, 3))) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { return 1; } return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 86ad6fd6b3e..919cda76c18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -71,6 +71,7 @@ import java.util.Collections; import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; @@ -135,6 +136,10 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter + // ISO 639-2 code back to the corresponding 2-letter code. + @Nullable private static HashMap languageTagIso3ToIso2; + private Util() {} /** @@ -450,18 +455,25 @@ public static void writeBoolean(Parcel parcel, boolean value) { if (language == null) { return null; } - try { - Locale locale = getLocaleForLanguageTag(language); - int localeLanguageLength = locale.getLanguage().length(); - String normLanguage = locale.getISO3Language(); - if (normLanguage.isEmpty()) { - return toLowerInvariant(language); - } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength)); - } catch (MissingResourceException e) { + Locale locale = getLocaleForLanguageTag(language); + String localeLanguage = locale.getLanguage(); + int localeLanguageLength = localeLanguage.length(); + if (localeLanguageLength == 0) { + // Return original language for invalid language tags. return toLowerInvariant(language); + } else if (localeLanguageLength == 3) { + // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter + // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + if (languageTagIso3ToIso2 == null) { + languageTagIso3ToIso2 = createIso3ToIso2Map(); + } + String iso2Language = languageTagIso3ToIso2.get(localeLanguage); + if (iso2Language != null) { + localeLanguage = iso2Language; + } } + String normTag = getLocaleLanguageTag(locale); + return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); } /** @@ -2013,6 +2025,54 @@ private static String getLocaleLanguageTagV21(Locale locale) { } } + private static HashMap createIso3ToIso2Map() { + String[] iso2Languages = Locale.getISOLanguages(); + HashMap iso3ToIso2 = + new HashMap<>( + /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + for (String iso2 : iso2Languages) { + try { + // This returns the ISO 639-2/T code for the language. + String iso3 = new Locale(iso2).getISO3Language(); + if (!TextUtils.isEmpty(iso3)) { + iso3ToIso2.put(iso3, iso2); + } + } catch (MissingResourceException e) { + // Shouldn't happen for list of known languages, but we don't want to throw either. + } + } + // Add additional ISO 639-2/B codes to mapping. + for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { + iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + } + return iso3ToIso2; + } + + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + private static final String[] iso3BibliographicalToIso2 = + new String[] { + "alb", "sq", + "arm", "hy", + "baq", "eu", + "bur", "my", + "tib", "bo", + "chi", "zh", + "cze", "cs", + "dut", "nl", + "ger", "de", + "gre", "el", + "fre", "fr", + "geo", "ka", + "ice", "is", + "mac", "mk", + "mao", "mi", + "may", "ms", + "per", "fa", + "rum", "ro", + "slo", "sk", + "wel", "cy" + }; + /** * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 9abec0cd8f2..f85ee37c079 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,14 +268,15 @@ public void testInflate() { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("spa-ar-dialect"); - assertThat(Util.normalizeLanguageCode("es-419")).isEqualTo("spa-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zho-hans-tw"); - assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zho-tw"); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } @@ -283,13 +284,38 @@ public void testNormalizeLanguageCodeV21() { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } + @Test + public void testNormalizeIso6392BibliographicalAndTextualCodes() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 095739271e3..254a2b2bd16 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -263,7 +263,7 @@ public void testPlaylistWithClosedCaption() throws IOException { Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4); - assertThat(closedCaptionFormat.language).isEqualTo("spa"); + assertThat(closedCaptionFormat.language).isEqualTo("es"); } @Test From 40fd11d9e8fe094a8cf2d3d66c2d00407c423897 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 16:18:49 +0100 Subject: [PATCH 176/219] Further language normalization tweaks for API < 21. 1. Using the Locale on API<21 doesn't make any sense because it's a no-op anyway. Slightly restructured the code to avoid that. 2. API<21 often reports languages with non-standard underscores instead of dashes. Normalize that too. 3. Some invalid language tags on API>21 get normalized to "und". Use original tag in such a case. Issue:#6153 PiperOrigin-RevId: 258773463 --- RELEASENOTES.md | 3 + .../google/android/exoplayer2/util/Util.java | 57 +++++++++---------- .../android/exoplayer2/util/UtilTest.java | 15 +++++ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f0813034e07..7bc7e1129be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,6 +10,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 919cda76c18..095394b2f54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -455,25 +455,31 @@ public static void writeBoolean(Parcel parcel, boolean value) { if (language == null) { return null; } - Locale locale = getLocaleForLanguageTag(language); - String localeLanguage = locale.getLanguage(); - int localeLanguageLength = localeLanguage.length(); - if (localeLanguageLength == 0) { - // Return original language for invalid language tags. - return toLowerInvariant(language); - } else if (localeLanguageLength == 3) { - // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter - // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + // Locale data (especially for API < 21) may produce tags with '_' instead of the + // standard-conformant '-'. + String normalizedTag = language.replace('_', '-'); + if (Util.SDK_INT >= 21) { + // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. + normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); + } + if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { + // Tag isn't valid, keep using the original. + normalizedTag = language; + } + normalizedTag = Util.toLowerInvariant(normalizedTag); + String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; + if (mainLanguage.length() == 3) { + // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO + // 639-1 codes automatically. if (languageTagIso3ToIso2 == null) { languageTagIso3ToIso2 = createIso3ToIso2Map(); } - String iso2Language = languageTagIso3ToIso2.get(localeLanguage); + String iso2Language = languageTagIso3ToIso2.get(mainLanguage); if (iso2Language != null) { - localeLanguage = iso2Language; + normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); } } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); + return normalizedTag; } /** @@ -1967,32 +1973,25 @@ private static void getDisplaySizeV16(Display display, Point outSize) { } private static String[] getSystemLocales() { + Configuration config = Resources.getSystem().getConfiguration(); return SDK_INT >= 24 - ? getSystemLocalesV24() - : new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)}; + ? getSystemLocalesV24(config) + : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()}; } @TargetApi(24) - private static String[] getSystemLocalesV24() { - return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ","); - } - - private static Locale getLocaleForLanguageTag(String languageTag) { - return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag); + private static String[] getSystemLocalesV24(Configuration config) { + return Util.split(config.getLocales().toLanguageTags(), ","); } @TargetApi(21) - private static Locale getLocaleForLanguageTagV21(String languageTag) { - return Locale.forLanguageTag(languageTag); - } - - private static String getLocaleLanguageTag(Locale locale) { - return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + private static String[] getSystemLocaleV21(Configuration config) { + return new String[] {config.locale.toLanguageTag()}; } @TargetApi(21) - private static String getLocaleLanguageTagV21(Locale locale) { - return locale.toLanguageTag(); + private static String normalizeLanguageCodeSyntaxV21(String languageTag) { + return Locale.forLanguageTag(languageTag).toLanguageTag(); } private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index f85ee37c079..5a13ed0dd85 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,10 +268,14 @@ public void testInflate() { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); @@ -284,9 +288,20 @@ public void testNormalizeLanguageCodeV21() { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + // Doesn't work on API < 21 because we can't use Locale syntax verification. + // assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } From 97e98ab4ed6a7bed687777a17e9a1c4d853c8a92 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Jul 2019 19:59:55 +0100 Subject: [PATCH 177/219] Cast: Remove obsolete flavor dimension PiperOrigin-RevId: 259582498 --- demos/cast/build.gradle | 11 ----------- demos/cast/src/main/AndroidManifest.xml | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 03a54947cfd..85e60f27969 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -47,17 +47,6 @@ android { // The demo app isn't indexed and doesn't have translations. disable 'GoogleAppIndexingWarning','MissingTranslation' } - - flavorDimensions "receiver" - - productFlavors { - defaultCast { - dimension "receiver" - manifestPlaceholders = - [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] - } - } - } dependencies { diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 856b0b1235c..dbfdd833f65 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:largeHeap="true" android:allowBackup="false"> + android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> Date: Fri, 26 Jul 2019 16:08:56 +0100 Subject: [PATCH 178/219] Add A10-70L to output surface workaround Issue: #6222 PiperOrigin-RevId: 260146226 --- .../google/android/exoplayer2/video/MediaCodecVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 8d5b890c7f7..591a10087c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1429,6 +1429,7 @@ protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { case "1713": case "1714": case "A10-70F": + case "A10-70L": case "A1601": case "A2016a40": case "A7000-a": From 0b756a9646e3e979a2645219acc4fd6fec960e2b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 18 Jul 2019 19:40:34 +0100 Subject: [PATCH 179/219] Merge pull request #6042 from Timbals:dev-v2 PiperOrigin-RevId: 258812820 --- RELEASENOTES.md | 2 + .../exoplayer2/demo/DemoDownloadService.java | 3 +- .../exoplayer2/offline/DownloadService.java | 39 ++++++++++-- .../exoplayer2/util/NotificationUtil.java | 28 +++++++-- .../ui/PlayerNotificationManager.java | 60 +++++++++++++++++-- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7bc7e1129be..5deb0c5168f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ * Fix issue where invalid language tags were normalized to "und" instead of keeping the original ([#6153](https://github.com/google/ExoPlayer/issues/6153)). +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. ### 2.10.3 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index 3886ef5c44e..c3909dfe46c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -41,7 +41,8 @@ public DemoDownloadService() { FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, CHANNEL_ID, - R.string.exo_download_notification_channel_name); + R.string.exo_download_notification_channel_name, + /* channelDescriptionResourceId= */ 0); nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; } 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 3900dc8e938..6587984f0c3 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 @@ -174,6 +174,7 @@ public abstract class DownloadService extends Service { @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; private DownloadManager downloadManager; private int lastStartId; @@ -214,7 +215,23 @@ protected DownloadService( foregroundNotificationId, foregroundNotificationUpdateInterval, /* channelId= */ null, - /* channelNameResourceId= */ 0); + /* channelNameResourceId= */ 0, + /* channelDescriptionResourceId= */ 0); + } + + /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */ + @Deprecated + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0); } /** @@ -230,25 +247,33 @@ protected DownloadService( * unique per package. The value may be truncated if it's too long. Ignored if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. * @param channelNameResourceId A string resource identifier for the user visible name of the - * channel, if {@code channelId} is specified. The recommended maximum length is 40 - * characters. The value may be truncated if it is too long. Ignored if {@code + * notification channel. The recommended maximum length is 40 characters. The value may be + * truncated if it's too long. Ignored if {@code channelId} is null or if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description of the notification channel, or 0 if no description is provided. The + * recommended maximum length is 300 characters. The value may be truncated if it is too long. + * Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. */ protected DownloadService( int foregroundNotificationId, long foregroundNotificationUpdateInterval, @Nullable String channelId, - @StringRes int channelNameResourceId) { + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { this.foregroundNotificationUpdater = null; this.channelId = null; this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; } else { this.foregroundNotificationUpdater = new ForegroundNotificationUpdater( foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; } } @@ -543,7 +568,11 @@ public static void startForeground(Context context, Class clazz = getClass(); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java index 4cd03f566d4..756494f9d0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java @@ -61,6 +61,14 @@ public final class NotificationUtil { /** @see NotificationManager#IMPORTANCE_HIGH */ public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; + /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */ + @Deprecated + public static void createNotificationChannel( + Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, id, nameResourceId, /* descriptionResourceId= */ 0, importance); + } + /** * Creates a notification channel that notifications can be posted to. See {@link * NotificationChannel} and {@link @@ -70,21 +78,33 @@ public final class NotificationUtil { * @param id The id of the channel. Must be unique per package. The value may be truncated if it's * too long. * @param nameResourceId A string resource identifier for the user visible name of the channel. - * You can rename this channel when the system locale changes by listening for the {@link - * Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters. - * The value may be truncated if it is too long. + * The recommended maximum length is 40 characters. The string may be truncated if it's too + * long. You can rename the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. + * @param descriptionResourceId A string resource identifier for the user visible description of + * the channel, or 0 if no description is provided. The recommended maximum length is 300 + * characters. The value may be truncated if it is too long. You can change the description of + * the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. * @param importance The importance of the channel. This controls how interruptive notifications * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. */ public static void createNotificationChannel( - Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + Context context, + String id, + @StringRes int nameResourceId, + @StringRes int descriptionResourceId, + @Importance int importance) { if (Util.SDK_INT >= 26) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel(id, context.getString(nameResourceId), importance); + if (descriptionResourceId != 0) { + channel.setDescription(context.getString(descriptionResourceId)); + } notificationManager.createNotificationChannel(channel); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index cedd3dbec51..260fb9d398a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -385,6 +385,26 @@ public void onBitmap(final Bitmap bitmap) { private boolean wasPlayWhenReady; private int lastPlaybackState; + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. @@ -397,8 +417,12 @@ public void onBitmap(final Bitmap bitmap) { * * @param context The {@link Context}. * @param channelId The id of the notification channel. - * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * @param channelName A string resource identifier for the user visible name of the notification + * channel. The recommended maximum length is 40 characters. The string may be truncated if + * it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * notification channel, or 0 if no description is provided. The recommended maximum length is + * 300 characters. The value may be truncated if it is too long. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. */ @@ -406,14 +430,37 @@ public static PlayerNotificationManager createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter); } + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter, NotificationListener)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable NotificationListener notificationListener) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter, + notificationListener); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last @@ -422,7 +469,9 @@ public static PlayerNotificationManager createWithNotificationChannel( * @param context The {@link Context}. * @param channelId The id of the notification channel. * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * recommended maximum length is 40 characters. The string may be truncated if it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * channel, or 0 if no description is provided. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param notificationListener The {@link NotificationListener}. @@ -431,11 +480,12 @@ public static PlayerNotificationManager createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); } From 70978cee78ff83959b7937c74b0902f0c25480e5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 26 Jul 2019 17:01:47 +0100 Subject: [PATCH 180/219] Update release notes --- RELEASENOTES.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5deb0c5168f..de4d474e5cf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,19 +2,20 @@ ### 2.10.4 ### -* Offline: Add Scheduler implementation which uses WorkManager. -* Flac extension: Parse `VORBIS_COMMENT` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). -* Fix issue where initial seek positions get ignored when playing a preroll ad. -* Fix `DataSchemeDataSource` re-opening and range requests - ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of keeping the original ([#6153](https://github.com/google/ExoPlayer/issues/6153)). -* Add ability to specify a description when creating notification channels via - ExoPlayer library classes. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Flac extension: Parse `VORBIS_COMMENT` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### From 95d2988490f7335474059154cdc7807150a253d1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Jul 2019 17:20:45 +0100 Subject: [PATCH 181/219] Fix handling of channel count changes with speed adjustment When using speed adjustment it was possible for playback to get stuck at a period transition when the channel count changed: SonicAudioProcessor would be drained at the point of the period transition in preparation for creating a new AudioTrack with the new channel count, but during draining the incorrect (new) channel count was used to calculate the output buffer size for pending data from Sonic. This meant that, for example, if the channel count changed from stereo to mono we could have an output buffer size that stored an non-integer number of audio frames, and in turn this would cause writing to the AudioTrack to get stuck as the AudioTrack would prevent writing a partial audio frame. Use Sonic's current channel count when draining output to fix the issue. PiperOrigin-RevId: 260156541 --- .../java/com/google/android/exoplayer2/audio/Sonic.java | 7 ++++--- .../android/exoplayer2/audio/SonicAudioProcessor.java | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index 0bf6baa4d0b..6cd46bb7052 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -30,6 +30,7 @@ private static final int MINIMUM_PITCH = 65; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; + private static final int BYTES_PER_SAMPLE = 2; private final int inputSampleRateHz; private final int channelCount; @@ -157,9 +158,9 @@ public void flush() { maxDiff = 0; } - /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ - public int getFramesAvailable() { - return outputFrameCount; + /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ + public int getOutputSize() { + return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } // Internal methods. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 0d938d33f48..bd32e5ee6f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -210,7 +210,7 @@ public void queueInput(ByteBuffer inputBuffer) { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getFramesAvailable() * channelCount * 2; + int outputSize = sonic.getOutputSize(); if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -243,7 +243,7 @@ public ByteBuffer getOutput() { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getOutputSize() == 0); } @Override From d76bf4bfcae925c2bd3799225f9ba5b8ca5aa96d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Jul 2019 18:07:02 +0100 Subject: [PATCH 182/219] Bump version to 2.10.4 PiperOrigin-RevId: 260164426 --- constants.gradle | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/constants.gradle b/constants.gradle index 70e77b22c68..9e532e053b9 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.3' - releaseVersionCode = 2010003 + releaseVersion = '2.10.4' + releaseVersionCode = 2010004 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 190f4de5a6c..f420f207679 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.3"; + public static final String VERSION = "2.10.4"; /** 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.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4"; /** * 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 = 2010003; + public static final int VERSION_INT = 2010004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 9c88e54837f4b9f7ac0ef885597d09c6b64e8115 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 21 May 2019 16:01:24 +0100 Subject: [PATCH 183/219] Deprecate JobDispatcherScheduler PiperOrigin-RevId: 249250184 --- extensions/jobdispatcher/README.md | 4 ++++ .../exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index f70125ba38a..bd768686250 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,7 +1,11 @@ # ExoPlayer Firebase JobDispatcher extension # +**DEPRECATED** Please use [WorkManager extension][] or [`PlatformScheduler`]. + This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. +[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md +[`PlatformScheduler`]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android ## Getting the extension ## diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index d79dead0d76..c8975275f15 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -54,7 +54,10 @@ * * @see GoogleApiAvailability + * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link + * com.google.android.exoplayer2.scheduler.PlatformScheduler}. */ +@Deprecated public final class JobDispatcherScheduler implements Scheduler { private static final boolean DEBUG = false; From 926ad198229fd19286a562790880699c4d916209 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 28 Jul 2019 20:26:55 +0100 Subject: [PATCH 184/219] Update README.md --- extensions/jobdispatcher/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index bd768686250..5d59e64466e 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,11 +1,13 @@ # ExoPlayer Firebase JobDispatcher extension # -**DEPRECATED** Please use [WorkManager extension][] or [`PlatformScheduler`]. +**This extension is deprecated. Please use [WorkManager extension][] or [PlatformScheduler][].** + +--- This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. [WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md -[`PlatformScheduler`]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android ## Getting the extension ## From e56deba9fe3bd5108a91bea9caa46e3297d2758e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 28 Jul 2019 20:27:35 +0100 Subject: [PATCH 185/219] Update README.md --- extensions/jobdispatcher/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index 5d59e64466e..c822c14ce8a 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,5 +1,7 @@ # ExoPlayer Firebase JobDispatcher extension # +--- + **This extension is deprecated. Please use [WorkManager extension][] or [PlatformScheduler][].** --- From d395db97df3f493fe9fe4913f81cf83d934eac38 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 28 Jul 2019 20:29:29 +0100 Subject: [PATCH 186/219] Update README.md --- extensions/jobdispatcher/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index c822c14ce8a..8be027d308a 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,10 +1,6 @@ # ExoPlayer Firebase JobDispatcher extension # ---- - -**This extension is deprecated. Please use [WorkManager extension][] or [PlatformScheduler][].** - ---- +**DEPRECATED: Please use [WorkManager extension][] or [PlatformScheduler][] instead.** This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. From d279c3d281a7affcbc72a4b81ad2dddf088e0303 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 28 Jul 2019 20:29:55 +0100 Subject: [PATCH 187/219] Update README.md --- extensions/jobdispatcher/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index 8be027d308a..712b76fb285 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,6 +1,6 @@ # ExoPlayer Firebase JobDispatcher extension # -**DEPRECATED: Please use [WorkManager extension][] or [PlatformScheduler][] instead.** +**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.** This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. From f5980a54a3f96537f8b905b9df47d722a6c3f8a0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 29 Jul 2019 16:08:37 +0100 Subject: [PATCH 188/219] Ensure the SilenceMediaSource position is in range Issue: #6229 PiperOrigin-RevId: 260500986 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/source/SilenceMediaSource.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index de4d474e5cf..16818c867ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index b03dd0ea7c8..72095c2c545 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -118,6 +118,7 @@ public long selectTracks( @NullableType SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -144,6 +145,7 @@ public long readDiscontinuity() { @Override public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < sampleStreams.size(); i++) { ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); } @@ -152,7 +154,7 @@ public long seekToUs(long positionUs) { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -172,6 +174,10 @@ public boolean continueLoading(long positionUs) { @Override public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } } private static final class SilenceSampleStream implements SampleStream { @@ -187,7 +193,7 @@ public SilenceSampleStream(long durationUs) { } public void seekTo(long positionUs) { - positionBytes = getAudioByteCount(positionUs); + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); } @Override From 8c1b60f2db09ce063d5f3815c74ee02f1d54a257 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 22:41:58 +0100 Subject: [PATCH 189/219] Tweak Firebase JobDispatcher extension README PiperOrigin-RevId: 260583198 --- extensions/jobdispatcher/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index 712b76fb285..a6f0c3966ac 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -24,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md - From 58e70e8351a2de018f41f91be63f388220268e01 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Jul 2019 16:14:06 +0100 Subject: [PATCH 190/219] Update javadoc for TrackOutput#sampleData to make it more clear that implementors aren't expected to rewind with setPosition() PiperOrigin-RevId: 260718614 --- .../com/google/android/exoplayer2/extractor/TrackOutput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index d7a1c753028..0d5a168197d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -119,7 +119,7 @@ int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. - * @param length The number of bytes to read. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. */ void sampleData(ParsableByteArray data, int length); From b5ca187e85930228fee353a29c270af3ea43049b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 16:44:19 +0100 Subject: [PATCH 191/219] Mp3Extractor: Avoid outputting seek frame as a sample This could previously occur when seeking back to position=0 PiperOrigin-RevId: 260933636 --- .../android/exoplayer2/extractor/mp3/Mp3Extractor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index c65ad0bc670..e42a10a75f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -117,6 +117,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; + private int firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -214,6 +215,10 @@ public int read(ExtractorInput input, PositionHolder seekPosition) /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = (int) input.getPosition(); + } else if (input.getPosition() == 0 && firstSamplePosition != 0) { + // Skip past the seek frame. + input.skipFully(firstSamplePosition); } return readSample(input); } From 3e99e7af547f625ad3a616538947e88adaf7e123 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 18:02:21 +0100 Subject: [PATCH 192/219] Clean up some Ogg comments & document granulePosition PiperOrigin-RevId: 260947018 --- .../extractor/ogg/DefaultOggSeeker.java | 28 +++++++++---------- .../extractor/ogg/OggPageHeader.java | 14 +++++++--- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c83662ee83e..9700760c49e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -147,12 +147,12 @@ public void resetSeeking() { * which it is sensible to just skip pages to the target granule and pre-roll instead of doing * another seek request. * - * @param targetGranule the target granule position to seek to. - * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule + * @param targetGranule The target granule position to seek to. + * @param input The {@link ExtractorInput} to read from. + * @return The position to seek the {@link ExtractorInput} to for a next call or -(currentGranule * + 2) if it's close enough to skip to the target page. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ @VisibleForTesting public long getNextSeekPosition(long targetGranule, ExtractorInput input) @@ -263,8 +263,8 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti * @param input The {@code ExtractorInput} to skip to the next page. * @param limit The limit up to which the search should take place. * @return Whether the next page was found. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. */ @VisibleForTesting boolean skipToNextPage(ExtractorInput input, long limit) @@ -321,14 +321,14 @@ long readGranuleOfLastPage(ExtractorInput input) throws IOException, Interrupted * Skips to the position of the start of the page containing the {@code targetGranule} and returns * the granule of the page previous to the target page. * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule. - * @param currentGranule the current granule or -1 if it's unknown. - * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior + * @param input The {@link ExtractorInput} to read from. + * @param targetGranule The target granule. + * @param currentGranule The current granule or -1 if it's unknown. + * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior * page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ @VisibleForTesting long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bbf7e2fc6bd..bb84909f676 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -38,7 +38,13 @@ public int revision; public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the end of the page. Samples partially in the page that continue on + * the next page do not count. + */ public long granulePosition; + public long streamSerialNumber; public long pageSequenceNumber; public long pageChecksum; @@ -72,10 +78,10 @@ public void reset() { * Peeks an Ogg page header and updates this {@link OggPageHeader}. * * @param input The {@link ExtractorInput} to read from. - * @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if - * something goes wrong. - * @return {@code true} if the read was successful. The read fails if the end of the input is - * encountered without reading data. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. * @throws IOException If reading data fails or the stream is invalid. * @throws InterruptedException If the thread is interrupted. */ From e159e3acd0ce45d41dc67ae001ab5bd2a08933cb Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 10:34:11 +0100 Subject: [PATCH 193/219] Mp3Extractor: Avoid outputting non-zero position seek frame as a sample Checking inputPosition == 0 isn't sufficient because the synchronization at the top of read() may advance the input (i.e. in the case where there's some garbage prior to the seek frame). PiperOrigin-RevId: 261086901 --- .../exoplayer2/extractor/mp3/Mp3Extractor.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index e42a10a75f6..bc218e26ad6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -117,7 +117,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; - private int firstSamplePosition; + private long firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -215,10 +215,13 @@ public int read(ExtractorInput input, PositionHolder seekPosition) /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); - firstSamplePosition = (int) input.getPosition(); - } else if (input.getPosition() == 0 && firstSamplePosition != 0) { - // Skip past the seek frame. - input.skipFully(firstSamplePosition); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } } return readSample(input); } From 309d043ceeb9d1adb392ebebf12f7e300b45780c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 1 Aug 2019 20:37:19 +0100 Subject: [PATCH 194/219] Merge pull request #6239 from ittiam-systems:vorbis-picture-parse PiperOrigin-RevId: 261087432 --- RELEASENOTES.md | 2 +- extensions/flac/proguard-rules.txt | 3 + .../exoplayer2/ext/flac/FlacExtractor.java | 4 +- extensions/flac/src/main/jni/flac_jni.cc | 40 ++++- extensions/flac/src/main/jni/flac_parser.cc | 21 +++ .../flac/src/main/jni/include/flac_parser.h | 23 ++- .../metadata/flac/PictureFrame.java | 144 ++++++++++++++++++ .../{vorbis => flac}/VorbisComment.java | 2 +- .../exoplayer2/util/FlacStreamMetadata.java | 32 ++-- .../metadata/flac/PictureFrameTest.java | 42 +++++ .../{vorbis => flac}/VorbisCommentTest.java | 2 +- .../util/FlacStreamMetadataTest.java | 14 +- .../android/exoplayer2/ui/PlayerView.java | 26 +++- 13 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java rename library/core/src/main/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisComment.java (97%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java rename library/core/src/test/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisCommentTest.java (96%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 16818c867ea..7fea201237f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,7 +16,7 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Ensure the `SilenceMediaSource` position is in range ([#6229](https://github.com/google/ExoPlayer/issues/6229)). -* Flac extension: Parse `VORBIS_COMMENT` metadata +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index b44dab34455..3e52f643e7c 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -12,3 +12,6 @@ -keep class com.google.android.exoplayer2.util.FlacStreamMetadata { *; } +-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame { + *; +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 151875c2c5e..cd91b062888 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -229,8 +229,8 @@ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedExcept binarySearchSeeker = outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.vorbisComments != null) { - metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.metadata != null) { + metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4ba071e1cae..d60a7cead2a 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -102,10 +102,10 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "", "()V"); jobject commentList = env->NewObject(arrayListClass, arrayListConstructor); + jmethodID arrayListAddMethod = + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); - if (context->parser->isVorbisCommentsValid()) { - jmethodID arrayListAddMethod = - env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + if (context->parser->areVorbisCommentsValid()) { std::vector vorbisComments = context->parser->getVorbisComments(); for (std::vector::const_iterator vorbisComment = @@ -117,21 +117,49 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } } + jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor); + bool picturesValid = context->parser->arePicturesValid(); + if (picturesValid) { + std::vector pictures = context->parser->getPictures(); + jclass pictureFrameClass = env->FindClass( + "com/google/android/exoplayer2/metadata/flac/PictureFrame"); + jmethodID pictureFrameConstructor = + env->GetMethodID(pictureFrameClass, "", + "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); + for (std::vector::const_iterator picture = pictures.begin(); + picture != pictures.end(); ++picture) { + jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); + jstring description = env->NewStringUTF(picture->description.c_str()); + jbyteArray pictureData = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(pictureData, 0, picture->data.size(), + (signed char *)&picture->data[0]); + jobject pictureFrame = env->NewObject( + pictureFrameClass, pictureFrameConstructor, picture->type, mimeType, + description, picture->width, picture->height, picture->depth, + picture->colors, pictureData); + env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame); + env->DeleteLocalRef(mimeType); + env->DeleteLocalRef(description); + env->DeleteLocalRef(pictureData); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); jclass flacStreamMetadataClass = env->FindClass( "com/google/android/exoplayer2/util/" "FlacStreamMetadata"); - jmethodID flacStreamMetadataConstructor = env->GetMethodID( - flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;)V"); + jmethodID flacStreamMetadataConstructor = + env->GetMethodID(flacStreamMetadataClass, "", + "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, streamInfo.min_blocksize, streamInfo.max_blocksize, streamInfo.min_framesize, streamInfo.max_framesize, streamInfo.sample_rate, streamInfo.channels, streamInfo.bits_per_sample, streamInfo.total_samples, - commentList); + commentList, pictureFrames); } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index b2d074252dd..830f3e2178a 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -191,6 +191,24 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } break; + case FLAC__METADATA_TYPE_PICTURE: { + const FLAC__StreamMetadata_Picture *parsedPicture = + &metadata->data.picture; + FlacPicture picture; + picture.mimeType.assign(std::string(parsedPicture->mime_type)); + picture.description.assign( + std::string((char *)parsedPicture->description)); + picture.data.assign(parsedPicture->data, + parsedPicture->data + parsedPicture->data_length); + picture.width = parsedPicture->width; + picture.height = parsedPicture->height; + picture.depth = parsedPicture->depth; + picture.colors = parsedPicture->colors; + picture.type = parsedPicture->type; + mPictures.push_back(picture); + mPicturesValid = true; + break; + } default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -253,6 +271,7 @@ FLACParser::FLACParser(DataSource *source) mEOF(false), mStreamInfoValid(false), mVorbisCommentsValid(false), + mPicturesValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -288,6 +307,8 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_SEEKTABLE); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_PICTURE); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index d9043e95487..14ba9e8725a 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -30,6 +30,17 @@ typedef int status_t; +struct FlacPicture { + int type; + std::string mimeType; + std::string description; + FLAC__uint32 width; + FLAC__uint32 height; + FLAC__uint32 depth; + FLAC__uint32 colors; + std::vector data; +}; + class FLACParser { public: FLACParser(DataSource *source); @@ -48,10 +59,14 @@ class FLACParser { return mStreamInfo; } - bool isVorbisCommentsValid() { return mVorbisCommentsValid; } + bool areVorbisCommentsValid() const { return mVorbisCommentsValid; } std::vector getVorbisComments() { return mVorbisComments; } + bool arePicturesValid() const { return mPicturesValid; } + + const std::vector &getPictures() const { return mPictures; } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -80,7 +95,9 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentsValid = false; + mPicturesValid = false; mVorbisComments.clear(); + mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -130,6 +147,10 @@ class FLACParser { std::vector mVorbisComments; bool mVorbisCommentsValid; + // cached when the PICTURE metadata is parsed by libFLAC + std::vector mPictures; + bool mPicturesValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java new file mode 100644 index 00000000000..ce134614ad1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.Arrays; + +/** A picture parsed from a FLAC file. */ +public final class PictureFrame implements Metadata.Entry { + + /** The type of the picture. */ + public final int pictureType; + /** The mime type of the picture. */ + public final String mimeType; + /** A description of the picture. */ + public final String description; + /** The width of the picture in pixels. */ + public final int width; + /** The height of the picture in pixels. */ + public final int height; + /** The color depth of the picture in bits-per-pixel. */ + public final int depth; + /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + String description, + int width, + int height, + int depth, + int colors, + byte[] pictureData) { + this.pictureType = pictureType; + this.mimeType = mimeType; + this.description = description; + this.width = width; + this.height = height; + this.depth = depth; + this.colors = colors; + this.pictureData = pictureData; + } + + /* package */ PictureFrame(Parcel in) { + this.pictureType = in.readInt(); + this.mimeType = castNonNull(in.readString()); + this.description = castNonNull(in.readString()); + this.width = in.readInt(); + this.height = in.readInt(); + this.depth = in.readInt(); + this.colors = in.readInt(); + this.pictureData = castNonNull(in.createByteArray()); + } + + @Override + public String toString() { + return "Picture: mimeType=" + mimeType + ", description=" + description; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PictureFrame other = (PictureFrame) obj; + return (pictureType == other.pictureType) + && mimeType.equals(other.mimeType) + && description.equals(other.description) + && (width == other.width) + && (height == other.height) + && (depth == other.depth) + && (colors == other.colors) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + depth; + result = 31 * result + colors; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pictureType); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(depth); + dest.writeInt(colors); + dest.writeByteArray(pictureData); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PictureFrame createFromParcel(Parcel in) { + return new PictureFrame(in); + } + + @Override + public PictureFrame[] newArray(int size) { + return new PictureFrame[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java index b1951cbc13e..9f44cdf393d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.android.exoplayer2.util.Util.castNonNull; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 43fdda367e5..2c814294af0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -18,7 +18,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import java.util.List; @@ -35,7 +36,7 @@ public final class FlacStreamMetadata { public final int channels; public final int bitsPerSample; public final long totalSamples; - @Nullable public final Metadata vorbisComments; + @Nullable public final Metadata metadata; private static final String SEPARATOR = "="; @@ -58,7 +59,7 @@ public FlacStreamMetadata(byte[] data, int offset) { this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.vorbisComments = null; + this.metadata = null; } /** @@ -71,10 +72,13 @@ public FlacStreamMetadata(byte[] data, int offset) { * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. * @param vorbisComments Vorbis comments. Each entry must be in key=value form. + * @param pictureFrames Picture frames. * @see FLAC format * METADATA_BLOCK_STREAMINFO * @see FLAC format * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE */ public FlacStreamMetadata( int minBlockSize, @@ -85,7 +89,8 @@ public FlacStreamMetadata( int channels, int bitsPerSample, long totalSamples, - List vorbisComments) { + List vorbisComments, + List pictureFrames) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -94,7 +99,7 @@ public FlacStreamMetadata( this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; - this.vorbisComments = parseVorbisComments(vorbisComments); + this.metadata = buildMetadata(vorbisComments, pictureFrames); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ @@ -138,22 +143,25 @@ public long getApproxBytesPerFrame() { } @Nullable - private static Metadata parseVorbisComments(@Nullable List vorbisComments) { - if (vorbisComments == null || vorbisComments.isEmpty()) { + private static Metadata buildMetadata( + List vorbisComments, List pictureFrames) { + if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { return null; } - ArrayList commentFrames = new ArrayList<>(); - for (String vorbisComment : vorbisComments) { + ArrayList metadataEntries = new ArrayList<>(); + for (int i = 0; i < vorbisComments.size(); i++) { + String vorbisComment = vorbisComments.get(i); String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); if (keyAndValue.length != 2) { Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); } else { - VorbisComment commentFrame = new VorbisComment(keyAndValue[0], keyAndValue[1]); - commentFrames.add(commentFrame); + VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); + metadataEntries.add(entry); } } + metadataEntries.addAll(pictureFrames); - return commentFrames.isEmpty() ? null : new Metadata(commentFrames); + return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java new file mode 100644 index 00000000000..3f07dbc26d3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link PictureFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class PictureFrameTest { + + @Test + public void testParcelable() { + PictureFrame pictureFrameToParcel = new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]); + + Parcel parcel = Parcel.obtain(); + pictureFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel); + assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java index 868b28b0e1e..bb118e381a7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.common.truth.Truth.assertThat; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java index 325d9b19f68..72a80161f2e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -19,7 +19,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +34,8 @@ public void parseVorbisComments() { commentsList.add("Title=Song"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; assertThat(metadata.length()).isEqualTo(2); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -49,7 +50,8 @@ public void parseVorbisComments() { public void parseEmptyVorbisComments() { ArrayList commentsList = new ArrayList<>(); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; assertThat(metadata).isNull(); } @@ -59,7 +61,8 @@ public void parseVorbisCommentWithEqualsInValue() { ArrayList commentsList = new ArrayList<>(); commentsList.add("Title=So=ng"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -73,7 +76,8 @@ public void parseInvalidVorbisComment() { commentsList.add("TitleSong"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index e6bc1a6a71b..1e7d6407e6a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -304,6 +305,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideOnTouch; private int textureViewRotation; private boolean isTouching; + private static final int PICTURE_TYPE_FRONT_COVER = 3; + private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { this(context, null); @@ -1246,15 +1249,32 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { } private boolean setArtworkFromMetadata(Metadata metadata) { + boolean isArtworkSet = false; + int currentPictureType = PICTURE_TYPE_NOT_SET; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); + int pictureType; + byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + bitmapData = ((ApicFrame) metadataEntry).pictureData; + pictureType = ((ApicFrame) metadataEntry).pictureType; + } else if (metadataEntry instanceof PictureFrame) { + bitmapData = ((PictureFrame) metadataEntry).pictureData; + pictureType = ((PictureFrame) metadataEntry).pictureType; + } else { + continue; + } + // Prefer the first front cover picture. If there aren't any, prefer the first picture. + if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + currentPictureType = pictureType; + if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { + break; + } } } - return false; + return isArtworkSet; } private boolean setDrawableArtwork(@Nullable Drawable drawable) { From 4438cdb282b4413c2b2ec0261094ebc95d86cc47 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 1 Aug 2019 12:15:59 +0100 Subject: [PATCH 195/219] return lg specific mime type as codec supported type for OMX.lge.alac.decoder ISSUE: #5938 PiperOrigin-RevId: 261097045 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 374c15eea05..4f59c19795b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -346,6 +346,13 @@ private static String getCodecSupportedType( boolean secureDecodersExplicit, String requestedMimeType) { if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(requestedMimeType)) { + return supportedType; + } + } + if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { // Handle decoders that declare support for DV via MIME types that aren't // video/dolby-vision. @@ -355,13 +362,12 @@ private static String getCodecSupportedType( || "OMX.realtek.video.decoder.tunneled".equals(name)) { return "video/dv_hevc"; } - } - - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) + && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) + && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; } } return null; From 740502103fac125a5ae948ce7b60fcf7d1baff54 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:05:07 +0100 Subject: [PATCH 196/219] Some no-op cleanup for DefaultOggSeeker PiperOrigin-RevId: 261102008 --- .../extractor/ogg/DefaultOggSeeker.java | 108 ++++++++---------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 9700760c49e..a4aa6b8dd5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -206,39 +207,32 @@ public long getNextSeekPosition(long targetGranule, ExtractorInput input) return -(pageHeader.granulePosition + 2); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; - } - if (position >= endPosition) { - position = endPosition - 1; - } - return position; - } - - private class OggSeekMap implements SeekMap { - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - if (timeUs == 0) { - return new SeekPoints(new SeekPoint(0, startPosition)); - } - long granule = streamReader.convertTimeToGranule(timeUs); - long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); - return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); - } - - @Override - public long getDurationUs() { - return streamReader.convertGranuleToTime(totalGranules); + /** + * Skips to the position of the start of the page containing the {@code targetGranule} and returns + * the granule of the page previous to the target page. + * + * @param input The {@link ExtractorInput} to read from. + * @param targetGranule The target granule. + * @param currentGranule The current granule or -1 if it's unknown. + * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior + * page. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + @VisibleForTesting + long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) + throws IOException, InterruptedException { + pageHeader.populate(input, false); + while (pageHeader.granulePosition < targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + // Store in a member field to be able to resume after IOExceptions. + currentGranule = pageHeader.granulePosition; + // Peek next header. + pageHeader.populate(input, false); } - + input.resetPeekPosition(); + return currentGranule; } /** @@ -266,8 +260,7 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti * @throws IOException If peeking/reading from the input fails. * @throws InterruptedException If interrupted while peeking/reading from the input. */ - @VisibleForTesting - boolean skipToNextPage(ExtractorInput input, long limit) + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { limit = Math.min(limit + 3, endPosition); byte[] buffer = new byte[2048]; @@ -317,32 +310,27 @@ long readGranuleOfLastPage(ExtractorInput input) throws IOException, Interrupted return pageHeader.granulePosition; } - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. - * - * @param input The {@link ExtractorInput} to read from. - * @param targetGranule The target granule. - * @param currentGranule The current granule or -1 if it's unknown. - * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. - * @throws ParserException If populating the page header fails. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) - throws IOException, InterruptedException { - pageHeader.populate(input, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); + private final class OggSeekMap implements SeekMap { + + @Override + public boolean isSeekable() { + return true; } - input.resetPeekPosition(); - return currentGranule; - } + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + startPosition + + (targetGranule * (endPosition - startPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } From 520275ec71fd886b5f50a834deaaf60038a1650e Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:06:45 +0100 Subject: [PATCH 197/219] Make OggSeeker.startSeek take a granule rather than a time PiperOrigin-RevId: 261102180 --- .../exoplayer2/extractor/ogg/DefaultOggSeeker.java | 5 ++--- .../android/exoplayer2/extractor/ogg/FlacReader.java | 6 ++---- .../android/exoplayer2/extractor/ogg/OggSeeker.java | 10 ++++------ .../android/exoplayer2/extractor/ogg/StreamReader.java | 9 +++++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index a4aa6b8dd5e..308547e5108 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -120,12 +120,11 @@ public long read(ExtractorInput input) throws IOException, InterruptedException } @Override - public long startSeek(long timeUs) { + public void startSeek(long targetGranule) { Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); + this.targetGranule = targetGranule; state = STATE_SEEK; resetSeeking(); - return targetGranule; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index d4c2bbb485d..4efd5c5e111 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -185,11 +185,9 @@ public long read(ExtractorInput input) throws IOException, InterruptedException } @Override - public long startSeek(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + public void startSeek(long targetGranule) { + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; - return granule; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index aa88e5bf899..e4c3a163e6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -33,16 +33,14 @@ SeekMap createSeekMap(); /** - * Initializes a seek operation. + * Starts a seek operation. * - * @param timeUs The seek position in microseconds. - * @return The granule position targeted by the seek. + * @param targetGranule The target granule position. */ - long startSeek(long timeUs); + void startSeek(long targetGranule); /** - * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a - * progressive seek. + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. *

    * If more data is required or if the position of the input needs to be modified then a position * from which data should be provided is returned. Else a negative value is returned. If a seek diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index e459ad1e584..35a07fcf49d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -91,7 +91,8 @@ final void seek(long position, long timeUs) { reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(timeUs); + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); state = STATE_READ_PAYLOAD; } } @@ -248,13 +249,13 @@ protected void onSeekEnd(long currentGranule) { private static final class UnseekableOggSeeker implements OggSeeker { @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { return -1; } @Override - public long startSeek(long timeUs) { - return 0; + public void startSeek(long targetGranule) { + // Do nothing. } @Override From 23ace1936984238e8dbf616ad5a1751687fcb0c2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:14:12 +0100 Subject: [PATCH 198/219] Standardize ALAC initialization data Android considers ALAC initialization data to consider of the magic cookie only, where-as FFmpeg requires a full atom. Standardize around the Android definition, since it makes more sense (the magic cookie being contained within an atom is container specific, where-as the decoder shouldn't care what container the media stream is carried in) Issue: #5938 PiperOrigin-RevId: 261124155 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 47 ++++++++++++++----- .../exoplayer2/extractor/mp4/AtomParsers.java | 6 +-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 7c5864420ac..35b67e10687 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -172,28 +172,49 @@ public int getSampleRate() { private static @Nullable byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: - case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_OPUS: return initializationData.get(0); + case MimeTypes.AUDIO_ALAC: + return getAlacExtraData(initializationData); case MimeTypes.AUDIO_VORBIS: - byte[] header0 = initializationData.get(0); - byte[] header1 = initializationData.get(1); - byte[] extraData = new byte[header0.length + header1.length + 6]; - extraData[0] = (byte) (header0.length >> 8); - extraData[1] = (byte) (header0.length & 0xFF); - System.arraycopy(header0, 0, extraData, 2, header0.length); - extraData[header0.length + 2] = 0; - extraData[header0.length + 3] = 0; - extraData[header0.length + 4] = (byte) (header1.length >> 8); - extraData[header0.length + 5] = (byte) (header1.length & 0xFF); - System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); - return extraData; + return getVorbisExtraData(initializationData); default: // Other codecs do not require extra data. return null; } } + private static byte[] getAlacExtraData(List initializationData) { + // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra + // data. initializationData[0] contains only the magic cookie, and so we need to package it into + // an ALAC atom. See: + // https://ffmpeg.org/doxygen/0.6/alac_8c.html + // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt + byte[] magicCookie = initializationData.get(0); + int alacAtomLength = 12 + magicCookie.length; + ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength); + alacAtom.putInt(alacAtomLength); + alacAtom.putInt(0x616c6163); // type=alac + alacAtom.putInt(0); // version=0, flags=0 + alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length); + return alacAtom.array(); + } + + private static byte[] getVorbisExtraData(List initializationData) { + byte[] header0 = initializationData.get(0); + byte[] header1 = initializationData.get(1); + byte[] extraData = new byte[header0.length + header1.length + 6]; + extraData[0] = (byte) (header0.length >> 8); + extraData[1] = (byte) (header0.length & 0xFF); + System.arraycopy(header0, 0, extraData, 2, header0.length); + extraData[header0.length + 2] = 0; + extraData[header0.length + 3] = 0; + extraData[header0.length + 4] = (byte) (header1.length >> 8); + extraData[header0.length + 5] = (byte) (header1.length & 0xFF); + System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); + return extraData; + } + private native long ffmpegInitialize( String codecName, @Nullable byte[] extraData, 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 6fb0ac68564..70873825e3f 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 @@ -1140,10 +1140,6 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); - } else if (childAtomType == Atom.TYPE_alac) { - initializationData = new byte[childAtomSize]; - parent.setPosition(childPosition); - parent.readBytes(initializationData, /* offset= */ 0, childAtomSize); } else if (childAtomType == Atom.TYPE_dOps) { // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Signature and the body of the dOps atom. @@ -1152,7 +1148,7 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); - } else if (childAtomSize == Atom.TYPE_dfLa) { + } else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) { int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); From 7162bd81537723c1f92137b3000e0e9c916541cb Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 18:39:20 +0100 Subject: [PATCH 199/219] Propagate non-standard MIME type aliases Issue: #5938 PiperOrigin-RevId: 261150349 --- RELEASENOTES.md | 6 +- .../audio/MediaCodecAudioRenderer.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 49 ++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 173 +++++++++--------- .../video/MediaCodecVideoRenderer.java | 6 +- 5 files changed, 121 insertions(+), 115 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7fea201237f..39cc6408077 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). * Fix issue where initial seek positions get ignored when playing a preroll ad ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of @@ -14,8 +16,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). -* Ensure the `SilenceMediaSource` position is in range - ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). * Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). 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 ace7ebbcc65..5cf40a6741b 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 @@ -393,7 +393,7 @@ protected void configureCodec( codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); passthroughEnabled = codecInfo.passthrough; - String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; + String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 2158f182b11..7fc748485b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -54,8 +54,15 @@ public final class MediaCodecInfo { public final @Nullable String mimeType; /** - * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this - * is a passthrough codec. + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + + /** + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not + * known. */ public final @Nullable CodecCapabilities capabilities; @@ -98,6 +105,7 @@ public static MediaCodecInfo newPassthroughInstance(String name) { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,10 @@ public static MediaCodecInfo newPassthroughInstance(String name) { * * @param name The name of the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. - * @return The created instance. - */ - public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities) { - return new MediaCodecInfo( - name, - mimeType, - capabilities, - /* passthrough= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Creates an instance. - * - * @param name The name of the {@link MediaCodec}. - * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. + * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or + * {@code null} if not known. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. @@ -136,22 +128,31 @@ public static MediaCodecInfo newInstance(String name, String mimeType, public static MediaCodecInfo newInstance( String name, String mimeType, - CodecCapabilities capabilities, + String codecMimeType, + @Nullable CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( - name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + forceDisableAdaptive, + forceSecure); } private MediaCodecInfo( String name, @Nullable String mimeType, + @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; + this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 4f59c19795b..a6391e4cc71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -161,24 +161,17 @@ public static synchronized List getDecoderInfos( Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure, tunneling) : new MediaCodecListCompatV16(); - ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } - if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { - // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. - CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling); - ArrayList eac3DecoderInfos = - getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3); - decoderInfos.addAll(eac3DecoderInfos); - } applyWorkarounds(mimeType, decoderInfos); List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos); @@ -249,13 +242,11 @@ public static Pair getCodecProfileAndLevel(@Nullable String co * * @param key The codec key. * @param mediaCodecList The codec list. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. * @return The codec information for usable codecs matching the specified key. * @throws DecoderQueryException If there was an error querying the available decoders. */ private static ArrayList getDecoderInfosInternal(CodecKey key, - MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { + MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; @@ -265,28 +256,27 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String name = codecInfo.getName(); - String supportedType = - getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType); - if (supportedType == null) { + String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (codecMimeType == null) { continue; } try { - CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); boolean tunnelingSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); boolean tunnelingRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { continue; } boolean secureSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); boolean secureRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } @@ -295,12 +285,18 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add( MediaCodecInfo.newInstance( - name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false)); + name, + mimeType, + codecMimeType, + capabilities, + forceDisableAdaptive, + /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { decoderInfos.add( MediaCodecInfo.newInstance( name + ".secure", mimeType, + codecMimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ true)); @@ -314,7 +310,7 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, } else { // Rethrow error querying primary codec capabilities, or secondary codec // capabilities if API level is greater than 23. - Log.e(TAG, "Failed to query codec " + name + " (" + supportedType + ")"); + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); throw e; } } @@ -328,48 +324,49 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, } /** - * Returns the codec's supported type for decoding {@code requestedMimeType} on the current - * device, or {@code null} if the codec can't be used. + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. * * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. - * @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if - * the codec can't be used. + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. */ @Nullable - private static String getCodecSupportedType( + private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, boolean secureDecodersExplicit, - String requestedMimeType) { - if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + String mimeType) { + if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { + return null; + } + + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; } + } - if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { - // Handle decoders that declare support for DV via MIME types that aren't - // video/dolby-vision. - if ("OMX.MS.HEVCDV.Decoder".equals(name)) { - return "video/hevcdv"; - } else if ("OMX.RTK.video.decoder".equals(name) - || "OMX.realtek.video.decoder.tunneled".equals(name)) { - return "video/dv_hevc"; - } - } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) - && "OMX.lge.alac.decoder".equals(name)) { - return "audio/x-lg-alac"; - } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) - && "OMX.lge.flac.decoder".equals(name)) { - return "audio/x-lg-flac"; + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; } + return null; } @@ -379,12 +376,14 @@ private static String getCodecSupportedType( * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. + * @param mimeType The MIME type. * @return Whether the specified codec is usable for decoding on the current device. */ - private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String requestedMimeType) { + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -392,11 +391,11 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S // Work around broken audio decoders. if (Util.SDK_INT < 21 && ("CIPAACDecoder".equals(name) - || "CIPMP3Decoder".equals(name) - || "CIPVorbisDecoder".equals(name) - || "CIPAMRNBDecoder".equals(name) - || "AACDecoder".equals(name) - || "MP3Decoder".equals(name))) { + || "CIPMP3Decoder".equals(name) + || "CIPVorbisDecoder".equals(name) + || "CIPAMRNBDecoder".equals(name) + || "AACDecoder".equals(name) + || "MP3Decoder".equals(name))) { return false; } @@ -405,7 +404,7 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && ("a70".equals(Util.DEVICE) - || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } @@ -414,17 +413,17 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S if (Util.SDK_INT == 16 && "OMX.qcom.audio.decoder.mp3".equals(name) && ("dlxu".equals(Util.DEVICE) // HTC Butterfly - || "protou".equals(Util.DEVICE) // HTC Desire X - || "ville".equals(Util.DEVICE) // HTC One S - || "villeplus".equals(Util.DEVICE) - || "villec2".equals(Util.DEVICE) - || Util.DEVICE.startsWith("gee") // LGE Optimus G - || "C6602".equals(Util.DEVICE) // Sony Xperia Z - || "C6603".equals(Util.DEVICE) - || "C6606".equals(Util.DEVICE) - || "C6616".equals(Util.DEVICE) - || "L36h".equals(Util.DEVICE) - || "SO-02E".equals(Util.DEVICE))) { + || "protou".equals(Util.DEVICE) // HTC Desire X + || "ville".equals(Util.DEVICE) // HTC One S + || "villeplus".equals(Util.DEVICE) + || "villec2".equals(Util.DEVICE) + || Util.DEVICE.startsWith("gee") // LGE Optimus G + || "C6602".equals(Util.DEVICE) // Sony Xperia Z + || "C6603".equals(Util.DEVICE) + || "C6606".equals(Util.DEVICE) + || "C6616".equals(Util.DEVICE) + || "L36h".equals(Util.DEVICE) + || "SO-02E".equals(Util.DEVICE))) { return false; } @@ -432,9 +431,9 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S if (Util.SDK_INT == 16 && "OMX.qcom.audio.decoder.aac".equals(name) && ("C1504".equals(Util.DEVICE) // Sony Xperia E - || "C1505".equals(Util.DEVICE) - || "C1604".equals(Util.DEVICE) // Sony Xperia E dual - || "C1605".equals(Util.DEVICE))) { + || "C1505".equals(Util.DEVICE) + || "C1604".equals(Util.DEVICE) // Sony Xperia E dual + || "C1605".equals(Util.DEVICE))) { return false; } @@ -443,13 +442,13 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 - || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge - || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ - || "SC-05G".equals(Util.DEVICE) // Galaxy S6 - || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active - || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge - || "SC-04G".equals(Util.DEVICE) - || "SCV31".equals(Util.DEVICE))) { + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { return false; } @@ -459,10 +458,10 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("d2") - || Util.DEVICE.startsWith("serrano") - || Util.DEVICE.startsWith("jflte") - || Util.DEVICE.startsWith("santos") - || Util.DEVICE.startsWith("t0"))) { + || Util.DEVICE.startsWith("serrano") + || Util.DEVICE.startsWith("jflte") + || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { return false; } @@ -473,7 +472,7 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S } // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType) + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 591a10087c5..b5a935c15f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -551,10 +551,12 @@ protected void configureCodec( Format format, MediaCrypto crypto, float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( format, + codecMimeType, codecMaxValues, codecOperatingRate, deviceNeedsNoPostProcessWorkaround, @@ -1111,6 +1113,7 @@ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunneling * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * * @param format The format of media. + * @param codecMimeType The MIME type handled by the codec. * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. @@ -1123,13 +1126,14 @@ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunneling @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat( Format format, + String codecMimeType, CodecMaxValues codecMaxValues, float codecOperatingRate, boolean deviceNeedsNoPostProcessWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. - mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); From 7ec7aab320eb13a5a1459cb7b158fac1b0c94fc5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Apr 2019 14:15:38 +0100 Subject: [PATCH 200/219] Move E-AC3 workaround out of MediaCodecUtil PiperOrigin-RevId: 244173887 --- .../exoplayer2/audio/MediaCodecAudioRenderer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 5cf40a6741b..07a1438519c 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 @@ -364,8 +364,17 @@ protected List getDecoderInfos( return Collections.singletonList(passthroughDecoderInfo); } } - return mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + List decoderInfos = + mediaCodecSelector.getDecoderInfos( + format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. + List eac3DecoderInfos = + mediaCodecSelector.getDecoderInfos( + MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos.addAll(eac3DecoderInfos); + } + return Collections.unmodifiableList(decoderInfos); } /** From c373ff0a1c72c39e811dea1cc0ddb1da3915c28f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 13:27:57 +0100 Subject: [PATCH 201/219] Don't print warning when skipping RIFF and FMT chunks They're not unexpected! PiperOrigin-RevId: 260907687 --- .../exoplayer2/extractor/wav/WavHeaderReader.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index c7b7a40ead3..d76d3f37eab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -122,11 +121,13 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("data")) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { From 6d20a5cf0cc080f2bff03c60fd8e5321faea2dac Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 19:54:12 +0100 Subject: [PATCH 202/219] WavExtractor: Skip to data start position if position reset to 0 PiperOrigin-RevId: 260970865 --- .../extractor/wav/WavExtractor.java | 2 ++ .../exoplayer2/extractor/wav/WavHeader.java | 28 +++++++++++++------ .../extractor/wav/WavHeaderReader.java | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 68d252e318b..d3114f9b694 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -87,6 +87,8 @@ public int read(ExtractorInput input, PositionHolder seekPosition) if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); extractorOutput.seekMap(wavHeader); + } else if (input.getPosition() == 0) { + input.skipFully(wavHeader.getDataStartPosition()); } long dataLimit = wavHeader.getDataLimit(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c60117be607..c7858dcd962 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -37,9 +37,9 @@ @C.PcmEncoding private final int encoding; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ + /** Position of the start of the sample data, in bytes. */ + private int dataStartPosition; + /** Total size of the sample data, in bytes. */ private long dataSize; public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, @@ -50,6 +50,7 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +58,33 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i /** * Sets the data start position and size in bytes of sample data in this WAV. * - * @param dataStartPosition The data start position in bytes. - * @param dataSize The data size in bytes. + * @param dataStartPosition The position of the start of the sample data, in bytes. + * @param dataSize The total size of the sample data, in bytes. */ - public void setDataBounds(long dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataSize) { this.dataStartPosition = dataStartPosition; this.dataSize = dataSize; } - /** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ + /** + * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if + * the data bounds have not been set. + */ + public int getDataStartPosition() { + return dataStartPosition; + } + + /** + * Returns the limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds + * have not been set. + */ public long getDataLimit() { return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; } /** Returns whether the data start position and size have been set. */ public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; + return dataStartPosition != C.POSITION_UNSET; } // SeekMap implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index d76d3f37eab..839a9e3d5c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -139,7 +139,7 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + wavHeader.setDataBounds((int) input.getPosition(), chunkHeader.size); } private WavHeaderReader() { From f5e92134af3c0f112ec8ad7644d7282b7e8e2ce8 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:32:29 +0100 Subject: [PATCH 203/219] Shorten data length if it exceeds length of input Issue: #6241 PiperOrigin-RevId: 261126968 --- RELEASENOTES.md | 2 + .../extractor/wav/WavExtractor.java | 6 +-- .../exoplayer2/extractor/wav/WavHeader.java | 38 +++++++++++-------- .../extractor/wav/WavHeaderReader.java | 13 +++++-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39cc6408077..9bc77f8cfc3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ tags instead of 3-letter ISO 639-2 language tags. * Ensure the `SilenceMediaSource` position is in range ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* Calculate correct duration for clipped WAV streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). * Fix issue where initial seek positions get ignored when playing a preroll ad ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index d3114f9b694..91097c9e5b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -91,10 +91,10 @@ public int read(ExtractorInput input, PositionHolder seekPosition) input.skipFully(wavHeader.getDataStartPosition()); } - long dataLimit = wavHeader.getDataLimit(); - Assertions.checkState(dataLimit != C.POSITION_UNSET); + long dataEndPosition = wavHeader.getDataEndPosition(); + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - long bytesLeft = dataLimit - input.getPosition(); + long bytesLeft = dataEndPosition - input.getPosition(); if (bytesLeft <= 0) { return Extractor.RESULT_END_OF_INPUT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c7858dcd962..6e3c5988a9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -33,17 +33,21 @@ private final int blockAlignment; /** Bits per sample for the audio data. */ private final int bitsPerSample; - /** The PCM encoding */ - @C.PcmEncoding - private final int encoding; + /** The PCM encoding. */ + @C.PcmEncoding private final int encoding; /** Position of the start of the sample data, in bytes. */ private int dataStartPosition; - /** Total size of the sample data, in bytes. */ - private long dataSize; - - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + /** Position of the end of the sample data (exclusive), in bytes. */ + private long dataEndPosition; + + public WavHeader( + int numChannels, + int sampleRateHz, + int averageBytesPerSecond, + int blockAlignment, + int bitsPerSample, + @C.PcmEncoding int encoding) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; @@ -51,6 +55,7 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i this.bitsPerSample = bitsPerSample; this.encoding = encoding; dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -59,11 +64,11 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i * Sets the data start position and size in bytes of sample data in this WAV. * * @param dataStartPosition The position of the start of the sample data, in bytes. - * @param dataSize The total size of the sample data, in bytes. + * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. */ - public void setDataBounds(int dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataEndPosition) { this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; + this.dataEndPosition = dataEndPosition; } /** @@ -75,11 +80,11 @@ public int getDataStartPosition() { } /** - * Returns the limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds - * have not been set. + * Returns the position of the end of the sample data (exclusive), in bytes, or {@link + * C#POSITION_UNSET} if the data bounds have not been set. */ - public long getDataLimit() { - return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; + public long getDataEndPosition() { + return dataEndPosition; } /** Returns whether the data start position and size have been set. */ @@ -96,12 +101,13 @@ public boolean isSeekable() { @Override public long getDurationUs() { - long numFrames = dataSize / blockAlignment; + long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } @Override public SeekPoints getSeekPoints(long timeUs) { + long dataSize = dataEndPosition - dataStartPosition; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 839a9e3d5c2..bbcb75aa2d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -91,8 +91,8 @@ public static WavHeader peek(ExtractorInput input) throws IOException, Interrupt // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, - bitsPerSample, encoding); + return new WavHeader( + numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding); } /** @@ -139,7 +139,14 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds((int) input.getPosition(), chunkHeader.size); + int dataStartPosition = (int) input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + wavHeader.setDataBounds(dataStartPosition, dataEndPosition); } private WavHeaderReader() { From 88b68e5902c52ccf407a074aabd587d9fb473110 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 1 Aug 2019 21:06:56 +0100 Subject: [PATCH 204/219] Fix ExoPlayerTest --- .../test/java/com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 440a84bacb8..2203b34e869 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -2625,7 +2625,7 @@ public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() t /* isDynamic= */ false, /* durationUs= */ 10_000_000, adPlaybackState)); - final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, null); AtomicReference playerReference = new AtomicReference<>(); AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); EventListener eventListener = From 80bc50b647b2cf3555f1ba4c2d4cf1900bba8858 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 1 Aug 2019 19:36:18 +0100 Subject: [PATCH 205/219] Revert to using header bitrate for CBR MP3s A previous change switched to calculation of the bitrate based on the first MPEG audio header in the stream. This had the effect of fixing seeking to be consistent with playing from the start for streams where every frame has the same padding value, but broke streams where the encoder (correctly) modifies the padding value to match the declared bitrate in the header. Issue: #6238 PiperOrigin-RevId: 261163904 --- RELEASENOTES.md | 8 +++++--- .../android/exoplayer2/extractor/MpegAudioHeader.java | 4 ---- library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump | 2 +- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9bc77f8cfc3..829f8b70df9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,8 +9,12 @@ tags instead of 3-letter ISO 639-2 language tags. * Ensure the `SilenceMediaSource` position is in range ([#6229](https://github.com/google/ExoPlayer/issues/6229)). -* Calculate correct duration for clipped WAV streams +* WAV: Calculate correct duration for clipped streams ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Fix issue where initial seek positions get ignored when playing a preroll ad ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of @@ -20,8 +24,6 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). -* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index 87bb9920828..e454bd51c89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -186,10 +186,6 @@ public static boolean populateHeader(int headerData, MpegAudioHeader header) { } } - // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that - // seeking to a given timestamp and playing from the start up to that timestamp give the same - // results for CBR streams. See also [internal: b/120390268]. - bitrate = 8 * frameSize * sampleRate / samplesPerFrame; String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index d4df3ffebae..96b0cd259c9 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index d4df3ffebae..96b0cd259c9 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index d4df3ffebae..96b0cd259c9 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index d4df3ffebae..96b0cd259c9 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: From 3c8c5a3346eb05db16a9125072b8d101b8e222e2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:20:35 +0100 Subject: [PATCH 206/219] Fix DefaultOggSeeker seeking - When in STATE_SEEK with targetGranule==0, seeking would exit without checking that the input was positioned at the correct place. - Seeking could fail due to trying to read beyond the end of the stream. - Seeking was not robust against IO errors during the skip phase that occurs after the binary search has sufficiently converged. PiperOrigin-RevId: 261317035 --- .../extractor/ogg/DefaultOggSeeker.java | 173 ++++++++---------- .../extractor/ogg/StreamReader.java | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 109 +++++------ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 95 +--------- 4 files changed, 128 insertions(+), 251 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 308547e5108..064bd5732d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ogg; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; @@ -35,11 +36,12 @@ private static final int STATE_SEEK_TO_END = 0; private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_SEEK = 2; - private static final int STATE_IDLE = 3; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; private final OggPageHeader pageHeader = new OggPageHeader(); - private final long startPosition; - private final long endPosition; + private final long payloadStartPosition; + private final long payloadEndPosition; private final StreamReader streamReader; private int state; @@ -55,26 +57,27 @@ /** * Constructs an OggSeeker. * - * @param startPosition Start position of the payload (inclusive). - * @param endPosition End position of the payload (exclusive). * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). * @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageGranulePosition The granule position of the first payload page. - * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the - * ogg stream. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. */ public DefaultOggSeeker( - long startPosition, - long endPosition, StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, long firstPayloadPageSize, long firstPayloadPageGranulePosition, boolean firstPayloadPageIsLastPage) { - Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); this.streamReader = streamReader; - this.startPosition = startPosition; - this.endPosition = endPosition; - if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) { + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { totalGranules = firstPayloadPageGranulePosition; state = STATE_IDLE; } else { @@ -91,7 +94,7 @@ public long read(ExtractorInput input) throws IOException, InterruptedException positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; if (lastPageSearchPosition > positionBeforeSeekToEnd) { return lastPageSearchPosition; } @@ -101,137 +104,110 @@ public long read(ExtractorInput input) throws IOException, InterruptedException state = STATE_IDLE; return positionBeforeSeekToEnd; case STATE_SEEK: - long currentGranule; - if (targetGranule == 0) { - currentGranule = 0; - } else { - long position = getNextSeekPosition(targetGranule, input); - if (position >= 0) { - return position; - } - currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); state = STATE_IDLE; - return -(currentGranule + 2); + return -(startGranule + 2); default: // Never happens. throw new IllegalStateException(); } } - @Override - public void startSeek(long targetGranule) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - this.targetGranule = targetGranule; - state = STATE_SEEK; - resetSeeking(); - } - @Override public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } - @VisibleForTesting - public void resetSeeking() { - start = startPosition; - end = endPosition; + @Override + public void startSeek(long targetGranule) { + this.targetGranule = targetGranule; + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; startGranule = 0; endGranule = totalGranules; } /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until a negative number is returned. If a - * negative number is returned the input is at a position which is before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. * - * @param targetGranule The target granule position to seek to. * @param input The {@link ExtractorInput} to read from. - * @return The position to seek the {@link ExtractorInput} to for a next call or -(currentGranule - * + 2) if it's close enough to skip to the target page. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { if (start == end) { - return -(startGranule + 2); + return C.POSITION_UNSET; } - long initialPosition = input.getPosition(); + long currentPosition = input.getPosition(); if (!skipToNextPage(input, end)) { - if (start == initialPosition) { + if (start == currentPosition) { throw new IOException("No ogg page can be found."); } return start; } - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); input.resetPeekPosition(); long granuleDistance = targetGranule - pageHeader.granulePosition; int pageSize = pageHeader.headerSize + pageHeader.bodySize; - if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { - if (granuleDistance < 0) { - end = initialPosition; - endGranule = pageHeader.granulePosition; - } else { - start = input.getPosition() + pageSize; - startGranule = pageHeader.granulePosition; - if (end - start + pageSize < MATCH_BYTE_RANGE) { - input.skipFully(pageSize); - return -(startGranule + 2); - } - } - - if (end - start < MATCH_BYTE_RANGE) { - end = start; - return start; - } + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; + } - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); } /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. + * Skips forward to the start of the page containing the {@code targetGranule}. * * @param input The {@link ExtractorInput} to read from. - * @param targetGranule The target granule. - * @param currentGranule The current granule or -1 if it's unknown. - * @return The granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. * @throws ParserException If populating the page header fails. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) + private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); while (pageHeader.granulePosition < targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } input.resetPeekPosition(); - return currentGranule; } /** @@ -244,7 +220,7 @@ long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentG */ @VisibleForTesting void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { - if (!skipToNextPage(input, endPosition)) { + if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); } @@ -261,7 +237,7 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti */ private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { - limit = Math.min(limit + 3, endPosition); + limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; while (true) { @@ -302,8 +278,8 @@ private boolean skipToNextPage(ExtractorInput input, long limit) long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { skipToNextPage(input); pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { - pageHeader.populate(input, false); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); input.skipFully(pageHeader.headerSize + pageHeader.bodySize); } return pageHeader.granulePosition; @@ -320,10 +296,11 @@ public boolean isSeekable() { public SeekPoints getSeekPoints(long timeUs) { long targetGranule = streamReader.convertTimeToGranule(timeUs); long estimatedPosition = - startPosition - + (targetGranule * (endPosition - startPosition) / totalGranules) + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) - DEFAULT_OFFSET; - estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index 35a07fcf49d..d2671125e4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -148,9 +148,9 @@ private int readHeaders(ExtractorInput input) throws IOException, InterruptedExc boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( + this, payloadStartPosition, input.getLength(), - this, firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, firstPayloadPageHeader.granulePosition, isLastPage); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index 8d1818845dd..fba358ea513 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -36,9 +35,9 @@ public final class DefaultOggSeekerTest { public void testSetupWithUnsetEndPositionFails() { try { new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ C.LENGTH_UNSET, /* streamReader= */ new TestStreamReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ C.LENGTH_UNSET, /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 1, /* firstPayloadPageIsLastPage= */ false); @@ -62,9 +61,9 @@ private void testSeeking(Random random) throws IOException, InterruptedException TestStreamReader streamReader = new TestStreamReader(); DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ testFile.data.length, /* streamReader= */ streamReader, + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, /* firstPayloadPageIsLastPage= */ false); @@ -78,70 +77,56 @@ private void testSeeking(Random random) throws IOException, InterruptedException input.setPosition((int) nextSeekPosition); } - // Test granule 0 from file start - assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); + // Test granule 0 from file start. + long granule = seekTo(input, oggSeeker, 0, 0); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - // Test granule 0 from file end - assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); + // Test granule 0 from file end. + granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - { // Test last granule - long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - assertThat( - (testFile.lastGranule > currentGranule && position > input.getPosition()) - || (testFile.lastGranule == currentGranule && position == input.getPosition())) - .isTrue(); - } - - { // Test exact granule - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - assertThat( - (pageHeader.granulePosition > currentGranule && position > input.getPosition()) - || (pageHeader.granulePosition == currentGranule - && position == input.getPosition())) - .isTrue(); - } + // Test last granule. + granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); + long position = testFile.data.length; + // TODO: Simplify this. + assertThat( + (testFile.lastGranule > granule && position > input.getPosition()) + || (testFile.lastGranule == granule && position == input.getPosition())) + .isTrue(); + + // Test exact granule. + input.setPosition(testFile.data.length / 2); + oggSeeker.skipToNextPage(input); + assertThat(pageHeader.populate(input, true)).isTrue(); + position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; + granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); + // TODO: Simplify this. + assertThat( + (pageHeader.granulePosition > granule && position > input.getPosition()) + || (pageHeader.granulePosition == granule && position == input.getPosition())) + .isTrue(); for (int i = 0; i < 100; i += 1) { long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); int initialPosition = random.nextInt(testFile.data.length); - - long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition); + granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); - - assertWithMessage("getNextSeekPosition() didn't leave input on a page start.") - .that(pageHeader.populate(input, true)) - .isTrue(); - - if (currentGranule == 0) { + if (granule == 0) { assertThat(currentPosition).isEqualTo(0); } else { int previousPageStart = testFile.findPreviousPageStart(currentPosition); input.setPosition(previousPageStart); - assertThat(pageHeader.populate(input, true)).isTrue(); - assertThat(currentGranule).isEqualTo(pageHeader.granulePosition); + pageHeader.populate(input, false); + assertThat(granule).isEqualTo(pageHeader.granulePosition); } input.setPosition((int) currentPosition); - oggSeeker.skipToPageOfGranule(input, targetGranule, -1); - long positionDiff = Math.abs(input.getPosition() - currentPosition); - - long granuleDiff = currentGranule - targetGranule; - if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) - && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { - fail( - "granuleDiff (" - + granuleDiff - + ") or positionDiff (" - + positionDiff - + ") is more than allowed."); - } + pageHeader.populate(input, false); + // The target granule should be within the current page. + assertThat(granule).isAtMost(targetGranule); + assertThat(targetGranule).isLessThan(pageHeader.granulePosition); } } @@ -149,18 +134,15 @@ private long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; + oggSeeker.startSeek(targetGranule); int count = 0; - oggSeeker.resetSeeking(); - - do { - input.setPosition((int) nextSeekPosition); - nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input); - + while (nextSeekPosition >= 0) { if (count++ > 100) { - fail("infinite loop?"); + fail("Seek failed to converge in 100 iterations"); } - } while (nextSeekPosition >= 0); - + input.setPosition((int) nextSeekPosition); + nextSeekPosition = oggSeeker.read(input); + } return -(nextSeekPosition + 2); } @@ -171,8 +153,7 @@ protected long preparePayload(ParsableByteArray packet) { } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { return false; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index d6691f50f88..25216022284 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -85,9 +85,9 @@ private static void skipToNextPage(ExtractorInput extractorInput) throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ extractorInput.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -99,87 +99,6 @@ private static void skipToNextPage(ExtractorInput extractorInput) } } - @Test - public void testSkipToPageOfGranule() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - // expect to be granule of the previous page returned as elapsedSamples - skipToPageOfGranule(input, 54000, 40000); - // expect to be at the start of the third page - assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); - } - - @Test - public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 40000, 20000); - // expect to be at the start of the second page - assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); - } - - @Test - public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 10000, -1); - assertThat(input.getPosition()).isEqualTo(0); - } - - private void skipToPageOfGranule(ExtractorInput input, long granule, - long elapsedSamplesExpected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) - .isEqualTo(elapsedSamplesExpected); - return; - } catch (FakeExtractorInput.SimulatedIOException e) { - input.resetPeekPosition(); - } - } - } - @Test public void testReadGranuleOfLastPage() throws IOException, InterruptedException { FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( @@ -204,7 +123,7 @@ public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, Inter assertReadGranuleOfLastPage(input, 60000); fail(); } catch (EOFException e) { - // ignored + // Ignored. } } @@ -216,7 +135,7 @@ public void testReadGranuleOfLastPageWithUnboundedLength() assertReadGranuleOfLastPage(input, 60000); fail(); } catch (IllegalArgumentException e) { - // ignored + // Ignored. } } @@ -224,9 +143,9 @@ private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -235,7 +154,7 @@ private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); break; } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored + // Ignored. } } } From 1da5689ea08a527efebcd9a0391703fa75d7dcc6 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:29:12 +0100 Subject: [PATCH 207/219] Improve extractor tests based on ExtractorAsserts - Test seeking to (timeUs=0, position=0), which should always work and produce the same output as initially reading from the start of the stream. - Reset the input when testing seeking, to ensure IO errors are simulated for this case. PiperOrigin-RevId: 261317898 --- .../exoplayer2/testutil/ExtractorAsserts.java | 17 +++++++++++++---- .../exoplayer2/testutil/FakeExtractorInput.java | 9 +++++++++ .../testutil/FakeExtractorOutput.java | 6 ++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index 3937dabcafe..a933121bc5e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -175,17 +175,26 @@ private static FakeExtractorOutput assertOutput( extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); } + // Seeking to (timeUs=0, position=0) should always work, and cause the same data to be output. + extractorOutput.clearTrackOutputs(); + input.reset(); + consumeTestData(extractor, input, /* timeUs= */ 0, extractorOutput, false); + if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); + } + + // If the SeekMap is seekable, test seeking to 4 positions in the stream. SeekMap seekMap = extractorOutput.seekMap; if (seekMap.isSeekable()) { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { + extractorOutput.clearTrackOutputs(); long timeUs = (durationUs * j) / 3; long position = seekMap.getSeekPoints(timeUs).first.position; + input.reset(); input.setPosition((int) position); - for (int i = 0; i < extractorOutput.numberOfTracks; i++) { - extractorOutput.trackOutputs.valueAt(i).clear(); - } - consumeTestData(extractor, input, timeUs, extractorOutput, false); extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index c467bd36afe..1a127eeab5e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -80,6 +80,15 @@ private FakeExtractorInput(byte[] data, boolean simulateUnknownLength, failedPeekPositions = new SparseBooleanArray(); } + /** Resets the input to its initial state. */ + public void reset() { + readPosition = 0; + peekPosition = 0; + partiallySatisfiedTargetPositions.clear(); + failedReadPositions.clear(); + failedPeekPositions.clear(); + } + /** * Sets the read and peek positions. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index c6543bd7a54..4022a0ccc17 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -70,6 +70,12 @@ public void seekMap(SeekMap seekMap) { this.seekMap = seekMap; } + public void clearTrackOutputs() { + for (int i = 0; i < numberOfTracks; i++) { + trackOutputs.valueAt(i).clear(); + } + } + public void assertEquals(FakeExtractorOutput expected) { assertThat(numberOfTracks).isEqualTo(expected.numberOfTracks); assertThat(tracksEnded).isEqualTo(expected.tracksEnded); From f497bb96100bf86ba15f01dfe6007757ab9e5b8e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:48:44 +0100 Subject: [PATCH 208/219] Move DefaultOggSeeker tests into a single class PiperOrigin-RevId: 261320318 --- .../extractor/ogg/DefaultOggSeekerTest.java | 137 ++++++++++++++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 162 ------------------ 2 files changed, 136 insertions(+), 163 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index fba358ea513..fd649f09248 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -20,8 +20,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; import java.io.IOException; import java.util.Random; import org.junit.Test; @@ -31,6 +35,8 @@ @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { @@ -55,6 +61,95 @@ public void testSeeking() throws IOException, InterruptedException { } } + @Test + public void testSkipToNextPage() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(4000); + } + + @Test + public void testSkipToNextPageOverlap() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(2046); + } + + @Test + public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(1); + } + + @Test + public void testSkipToNextPageNoMatch() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + @Test + public void testReadGranuleOfLastPage() throws IOException, InterruptedException { + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(100, random), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random)), + false); + assertReadGranuleOfLastPage(input, 60000); + } + + @Test + public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // Ignored. + } + } + + @Test + public void testReadGranuleOfLastPageWithUnboundedLength() + throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // Ignored. + } + } + private void testSeeking(Random random) throws IOException, InterruptedException { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); @@ -130,7 +225,47 @@ private void testSeeking(Random random) throws IOException, InterruptedException } } - private long seekTo( + private static void skipToNextPage(ExtractorInput extractorInput) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + oggSeeker.skipToNextPage(extractorInput); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + /* ignored */ + } + } + } + + private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // Ignored. + } + } + } + + private static long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java deleted file mode 100644 index 25216022284..00000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.extractor.ogg; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.OggTestData; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.EOFException; -import java.io.IOException; -import java.util.Random; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultOggSeeker} utility methods. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultOggSeekerUtilMethodsTest { - - private final Random random = new Random(0); - - @Test - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(4000); - } - - @Test - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(2046); - } - - @Test - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - new byte[] {'x', 'O', 'g', 'g', 'S'} - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(1); - } - - @Test - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* streamReader= */ new FlacReader(), - /* payloadStartPosition= */ 0, - /* payloadEndPosition= */ extractorInput.getLength(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - oggSeeker.skipToNextPage(extractorInput); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } - } - } - - @Test - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - OggTestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random) - ), false); - assertReadGranuleOfLastPage(input, 60000); - } - - @Test - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // Ignored. - } - } - - @Test - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // Ignored. - } - } - - private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* streamReader= */ new FlacReader(), - /* payloadStartPosition= */ 0, - /* payloadEndPosition= */ input.getLength(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // Ignored. - } - } - } - -} From cd7fe05db72011154508087753f85cb198981a45 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 16:49:17 +0100 Subject: [PATCH 209/219] Constraint seek targetGranule within bounds + simplify tests PiperOrigin-RevId: 261328701 --- .../extractor/ogg/DefaultOggSeeker.java | 8 +-- .../extractor/ogg/DefaultOggSeekerTest.java | 26 ++------- .../exoplayer2/extractor/ogg/OggTestFile.java | 58 +++++++++++-------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 064bd5732d5..51ab94ba0ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -29,8 +29,8 @@ /** Seeks in an Ogg stream. */ /* package */ final class DefaultOggSeeker implements OggSeeker { - @VisibleForTesting public static final int MATCH_RANGE = 72000; - @VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000; + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; private static final int DEFAULT_OFFSET = 30000; private static final int STATE_SEEK_TO_END = 0; @@ -127,7 +127,7 @@ public OggSeekMap createSeekMap() { @Override public void startSeek(long targetGranule) { - this.targetGranule = targetGranule; + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); state = STATE_SEEK; start = payloadStartPosition; end = payloadEndPosition; @@ -201,7 +201,7 @@ private long getNextSeekPosition(ExtractorInput input) throws IOException, Inter private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { pageHeader.populate(input, /* quiet= */ false); - while (pageHeader.granulePosition < targetGranule) { + while (pageHeader.granulePosition <= targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); start = input.getPosition(); startGranule = pageHeader.granulePosition; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index fd649f09248..8ba0be26a05 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -160,7 +160,7 @@ private void testSeeking(Random random) throws IOException, InterruptedException /* payloadStartPosition= */ 0, /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, + /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -183,28 +183,12 @@ private void testSeeking(Random random) throws IOException, InterruptedException assertThat(input.getPosition()).isEqualTo(0); // Test last granule. - granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - // TODO: Simplify this. - assertThat( - (testFile.lastGranule > granule && position > input.getPosition()) - || (testFile.lastGranule == granule && position == input.getPosition())) - .isTrue(); - - // Test exact granule. - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - // TODO: Simplify this. - assertThat( - (pageHeader.granulePosition > granule && position > input.getPosition()) - || (pageHeader.granulePosition == granule && position == input.getPosition())) - .isTrue(); + granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0); + assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount); + assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize); for (int i = 0; i < 100; i += 1) { - long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); + long targetGranule = random.nextInt(testFile.granuleCount); int initialPosition = random.nextInt(testFile.data.length); granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index e5512dda363..38e4332b161 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -30,35 +30,39 @@ private static final int MAX_GRANULES_IN_PAGE = 100000; public final byte[] data; - public final long lastGranule; - public final int packetCount; + public final int granuleCount; public final int pageCount; public final int firstPayloadPageSize; - public final long firstPayloadPageGranulePosition; + public final int firstPayloadPageGranuleCount; + public final int lastPayloadPageSize; + public final int lastPayloadPageGranuleCount; private OggTestFile( byte[] data, - long lastGranule, - int packetCount, + int granuleCount, int pageCount, int firstPayloadPageSize, - long firstPayloadPageGranulePosition) { + int firstPayloadPageGranuleCount, + int lastPayloadPageSize, + int lastPayloadPageGranuleCount) { this.data = data; - this.lastGranule = lastGranule; - this.packetCount = packetCount; + this.granuleCount = granuleCount; this.pageCount = pageCount; this.firstPayloadPageSize = firstPayloadPageSize; - this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; + this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount; + this.lastPayloadPageSize = lastPayloadPageSize; + this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount; } public static OggTestFile generate(Random random, int pageCount) { ArrayList fileData = new ArrayList<>(); int fileSize = 0; - long granule = 0; - int packetLength = -1; - int packetCount = 0; + int granuleCount = 0; int firstPayloadPageSize = 0; - long firstPayloadPageGranulePosition = 0; + int firstPayloadPageGranuleCount = 0; + int lastPageloadPageSize = 0; + int lastPayloadPageGranuleCount = 0; + int packetLength = -1; for (int i = 0; i < pageCount; i++) { int headerType = 0x00; @@ -71,17 +75,17 @@ public static OggTestFile generate(Random random, int pageCount) { if (i == pageCount - 1) { headerType |= 4; } - granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; + int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + granuleCount += pageGranuleCount; + byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount); fileData.add(header); - fileSize += header.length; + int pageSize = header.length; byte[] laces = new byte[pageSegmentCount]; int bodySize = 0; for (int j = 0; j < pageSegmentCount; j++) { if (packetLength < 0) { - packetCount++; if (i < pageCount - 1) { packetLength = random.nextInt(MAX_PACKET_LENGTH); } else { @@ -96,14 +100,19 @@ public static OggTestFile generate(Random random, int pageCount) { packetLength -= 255; } fileData.add(laces); - fileSize += laces.length; + pageSize += laces.length; byte[] payload = TestUtil.buildTestData(bodySize, random); fileData.add(payload); - fileSize += payload.length; + pageSize += payload.length; + + fileSize += pageSize; if (i == 0) { - firstPayloadPageSize = header.length + bodySize; - firstPayloadPageGranulePosition = granule; + firstPayloadPageSize = pageSize; + firstPayloadPageGranuleCount = pageGranuleCount; + } else if (i == pageCount - 1) { + lastPageloadPageSize = pageSize; + lastPayloadPageGranuleCount = pageGranuleCount; } } @@ -115,11 +124,12 @@ public static OggTestFile generate(Random random, int pageCount) { } return new OggTestFile( file, - granule, - packetCount, + granuleCount, pageCount, firstPayloadPageSize, - firstPayloadPageGranulePosition); + firstPayloadPageGranuleCount, + lastPageloadPageSize, + lastPayloadPageGranuleCount); } public int findPreviousPageStart(long position) { From f2cff05c6914b1f987120e11ab4aedf05de210e7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:02:21 +0100 Subject: [PATCH 210/219] Remove obsolete workaround PiperOrigin-RevId: 261340526 --- build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build.gradle b/build.gradle index bc538ead68f..1d0b459bf5b 100644 --- a/build.gradle +++ b/build.gradle @@ -21,14 +21,6 @@ buildscript { classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } - // Workaround for the following test coverage issue. Remove when fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - configurations.all { - resolutionStrategy { - force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' - force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' - } - } } allprojects { repositories { From f3e5aaae3dddf779aa26be1d8f3cb7fd71a6596c Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:05:17 +0100 Subject: [PATCH 211/219] Upgrade dependency versions PiperOrigin-RevId: 261341256 --- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/workmanager/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index e067789bc41..83e994c5e1c 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.2.0' + api 'com.google.android.gms:play-services-cast-framework:17.0.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 76972a35301..34ad80b7ed1 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:73.3683.76' + api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'library') diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 9065855a3fc..ea7564316f8 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.0.1' + implementation 'androidx.work:work-runtime:2.1.0' } ext { From b0c2b1a0fa762a9c09ecad0c300193dd768827ce Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 19:03:03 +0100 Subject: [PATCH 212/219] Bump annotations dependency PiperOrigin-RevId: 261353271 --- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 2 +- extensions/ima/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/opus/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 2 +- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- testutils_robolectric/build.gradle | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 33161b4121c..124555d9b5a 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 7089d4d7314..06c5d1ffb72 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,7 +62,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 83e994c5e1c..68a7494a3fe 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:17.0.0' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 34ad80b7ed1..b2dd6bc8897 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ffecdcd16f1..15952b1860e 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 10b244cb39a..c67de276979 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 50acd6c0404..1031d6f4b76 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 2df9448d081..0ef9f281c90 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,7 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index c6f5a216ce3..ecaa78e25b1 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index db2e073c8ae..68bd422185a 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 0795079c6b1..28f7b05465f 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index ca734c36572..b74be659eed 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 02b68b831d6..92450f03810 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index 68ff8cc9771..e633e120575 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f6981a22204..9f5775d478f 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8e9696af70f..82e09ab72c7 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index a2e81fb3041..fa67ea1d012 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 6384bf920f3..5182dfccf5c 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index dd5cfa64a78..5865d3c36dd 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.annotation:annotation:1.1.0' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index bdc26d5c195..1ec358b83d7 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'androidx.test.ext:truth:' + androidXTestVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index a3859a9e489..758d22b5d91 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,5 +41,5 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' } From 936a7789c98484616bef4df30bed8ccf79786734 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 10:43:50 +0100 Subject: [PATCH 213/219] Check if controller is used when performing click directly. Issue:#6260 PiperOrigin-RevId: 261647858 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/ui/PlayerView.java | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 829f8b70df9..d9f534a4c88 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.3 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 1e7d6407e6a..95d2e1c1bb8 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -1156,6 +1156,9 @@ public View[] getAdOverlayViews() { // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1492,9 +1495,6 @@ public void onLayoutChange( @Override public boolean onSingleTapUp(MotionEvent e) { - if (!useController || player == null) { - return false; - } return toggleControllerVisibility(); } } From d1ac2727a6e1d2928d203791c6453908e77038e5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 16:57:44 +0100 Subject: [PATCH 214/219] Update stale TrackSelections in chunk sources when keeping the streams. If we keep streams in chunk sources after selecting new tracks, we also keep a reference to a stale disabled TrackSelection object. Fix this by updating the TrackSelection object when keeping the stream. The static part of the selection (i.e. the subset of selected tracks) stays the same in all cases. Issue:#6256 PiperOrigin-RevId: 261696082 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/source/MediaPeriod.java | 5 ++++- .../exoplayer2/source/dash/DashChunkSource.java | 7 +++++++ .../exoplayer2/source/dash/DashMediaPeriod.java | 16 +++++++++++++--- .../source/dash/DefaultDashChunkSource.java | 7 ++++++- .../exoplayer2/source/hls/HlsChunkSource.java | 10 ++++------ .../source/hls/HlsSampleStreamWrapper.java | 17 ++++++++++------- .../smoothstreaming/DefaultSsChunkSource.java | 7 ++++++- .../source/smoothstreaming/SsChunkSource.java | 7 +++++++ .../source/smoothstreaming/SsMediaPeriod.java | 1 + 10 files changed, 61 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d9f534a4c88..a3f6c1ebfc6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,9 @@ * Fix issue when calling `performClick` on `PlayerView` without `PlayerControlView` ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). ### 2.10.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index b40bbb35d12..c84847f7555 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -106,13 +106,16 @@ default List getStreamKeys(List trackSelections) { * Performs a track selection. * *

    The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} - * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * indicating whether the existing {@link SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * + *

    Note that previously received {@link TrackSelection TrackSelections} are no longer valid and + * references need to be replaced even if the corresponding {@link SampleStream} is kept. + * *

    This method is only called after the period has been prepared. * * @param selections The renderer track selections. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 40d4e468bdb..f7edf62182a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -69,4 +69,11 @@ DashChunkSource createDashChunkSource( * @param newManifest The new manifest. */ void updateManifest(DashManifest newManifest, int periodIndex); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 431a0a4bd9c..8635005bfca 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -402,17 +402,27 @@ private void selectNewStreams( int[] streamIndexToTrackGroupIndex) { // Create newly selected primary and event streams. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + if (streams[i] == null) { + // Create new stream for selection. streamResetFlags[i] = true; int trackGroupIndex = streamIndexToTrackGroupIndex[i]; TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs); } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); + Format format = selection.getTrackGroup().getFormat(0); streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); } + } else if (streams[i] instanceof ChunkSampleStream) { + // Update selection in existing stream. + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + stream.getChunkSource().updateTrackSelection(selection); } } // Create newly selected embedded streams from the corresponding primary stream. Note that this diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 057f0262d01..396d16968f8 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -111,7 +111,6 @@ public DashChunkSource createDashChunkSource( private final LoaderErrorThrower manifestLoaderErrorThrower; private final int[] adaptationSetIndices; - private final TrackSelection trackSelection; private final int trackType; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -120,6 +119,7 @@ public DashChunkSource createDashChunkSource( protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public void updateManifest(DashManifest newManifest, int newPeriodIndex) { } } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + @Override public void maybeThrowError() throws IOException { if (fatalError != null) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 261c9b531cd..ee5a5f0809b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -183,17 +183,15 @@ public TrackGroup getTrackGroup() { } /** - * Selects tracks for use. + * Sets the current track selection. * - * @param trackSelection The track selection. + * @param trackSelection The {@link TrackSelection}. */ - public void selectTracks(TrackSelection trackSelection) { + public void setTrackSelection(TrackSelection trackSelection) { this.trackSelection = trackSelection; } - /** - * Returns the current track selection. - */ + /** Returns the current {@link TrackSelection}. */ public TrackSelection getTrackSelection() { return trackSelection; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 434b6c20114..f7bc9135276 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -292,14 +292,17 @@ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStre TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; // Select new tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + if (trackGroupIndex == primaryTrackGroupIndex) { + primaryTrackSelection = selection; + chunkSource.setTrackSelection(selection); + } + if (streams[i] == null) { enabledTrackGroupCount++; - TrackSelection selection = selections[i]; - int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - if (trackGroupIndex == primaryTrackGroupIndex) { - primaryTrackSelection = selection; - chunkSource.selectTracks(selection); - } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; if (trackGroupToSampleQueueIndex != null) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 59e18195e20..22dfb04f130 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -74,10 +74,10 @@ public SsChunkSource createChunkSource( private final LoaderErrorThrower manifestLoaderErrorThrower; private final int streamElementIndex; - private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; + private TrackSelection trackSelection; private SsManifest manifest; private int currentManifestChunkOffset; @@ -155,6 +155,11 @@ public void updateManifest(SsManifest newManifest) { manifest = newManifest; } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + // ChunkSource implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index b763a484b80..111393140e5 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -55,4 +55,11 @@ SsChunkSource createChunkSource( * @param newManifest The new manifest. */ void updateManifest(SsManifest newManifest); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 135ee4a58e4..e325439d056 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -126,6 +126,7 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } From 8fdae6de18c1e1941371c79783521370088004fb Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Fri, 23 Aug 2019 16:48:32 -0700 Subject: [PATCH 215/219] WIP: Add logging to MediaCodecAudioRenderer... To diagnose the MusicChoice playback issues. --- demos/main/src/main/AndroidManifest.xml | 1 + .../exoplayer2/demo/PlayerActivity.java | 300 +++++++++++++++--- .../demo/SampleChooserActivity.java | 28 +- .../res/layout/sample_chooser_activity.xml | 13 +- .../src/main/res/layout/sample_list_item.xml | 12 +- .../main/res/xml/network_security_config.xml | 8 + .../audio/MediaCodecAudioRenderer.java | 3 + 7 files changed, 294 insertions(+), 71 deletions(-) create mode 100644 demos/main/src/main/res/xml/network_security_config.xml diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 355ba434058..c1208bfa50e 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" + android:networkSecurityConfig="@xml/network_security_config" android:name="com.google.android.exoplayer2.demo.DemoApplication" tools:ignore="UnusedAttribute"> 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 35307eb5d8e..5894ac081ca 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 @@ -15,13 +15,12 @@ */ package com.google.android.exoplayer2.demo; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.View; @@ -30,14 +29,22 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; +import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioCapabilitiesReceiver; +import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; @@ -55,6 +62,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.DefaultHlsExtractorFactory; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; @@ -72,6 +80,8 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; + +import java.io.IOException; import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; @@ -87,12 +97,15 @@ public class PlayerActivity extends AppCompatActivity public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String ENABLE_TUNNELED_PLAYBACK = "enable_tunneled_playback"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String EXTENSION_EXTRA = "extension"; public static final String ACTION_VIEW_LIST = "com.google.android.exoplayer.demo.action.VIEW_LIST"; + public static final String ACTION_CHANNEL_LIST = + "com.google.android.exoplayer.demo.action.CHANNEL_LIST"; public static final String URI_LIST_EXTRA = "uri_list"; public static final String EXTENSION_LIST_EXTRA = "extension_list"; @@ -141,11 +154,28 @@ public class PlayerActivity extends AppCompatActivity private int startWindow; private long startPosition; + private int currentChannel; + private Uri[] channelUris; + + private AudioCapabilitiesReceiver audioChangeReceiver; + // Fields used only for ad playback. The ads loader is loaded via reflection. private AdsLoader adsLoader; private Uri loadedAdTagUri; + private class AudioHotplugListener implements AudioCapabilitiesReceiver.Listener { + + @Override + public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + Log.d("ExoPlayer", "audio hotplug. new capabilities " + audioCapabilities); + if (player != null && trackSelector != null) { + trackSelector.buildUponParameters().clearSelectionOverrides(); + recreatePlayer(); + } + } + } + // Activity lifecycle @Override @@ -195,6 +225,10 @@ public void onCreate(Bundle savedInstanceState) { trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); clearStartPosition(); } + +// playerView.setControllerShowTimeoutMs(0); + audioChangeReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), new AudioHotplugListener()); + audioChangeReceiver.register(); } @Override @@ -210,7 +244,7 @@ public void onNewIntent(Intent intent) { public void onStart() { super.onStart(); if (Util.SDK_INT > 23) { - initializePlayer(); + initializePlayer(true); if (playerView != null) { playerView.onResume(); } @@ -221,7 +255,7 @@ public void onStart() { public void onResume() { super.onResume(); if (Util.SDK_INT <= 23 || player == null) { - initializePlayer(); + initializePlayer(true); if (playerView != null) { playerView.onResume(); } @@ -265,7 +299,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis return; } if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initializePlayer(); + initializePlayer(true); } else { showToast(R.string.storage_permission_denied); finish(); @@ -288,7 +322,33 @@ public void onSaveInstanceState(Bundle outState) { @Override public boolean dispatchKeyEvent(KeyEvent event) { // See whether the player view wants to handle media or DPAD keys events. - return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + boolean handled = false; + + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Uri nextChannel = null; + + switch (event.getKeyCode()) { + + case KeyEvent.KEYCODE_CHANNEL_DOWN: + if (channelUris != null) { + currentChannel = (currentChannel + (channelUris.length - 1)) % channelUris.length; + nextChannel = channelUris[currentChannel]; + } + break; + case KeyEvent.KEYCODE_CHANNEL_UP: + if (channelUris != null) { + currentChannel = (currentChannel + 1) % channelUris.length; + nextChannel = channelUris[currentChannel]; + } + break; + } + + if (nextChannel != null) { + playUnencryptedUri(nextChannel); + } + } + + return handled || playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); } // OnClickListener methods @@ -323,30 +383,33 @@ public void onVisibilityChange(int visibility) { // Internal methods - private void initializePlayer() { + private void initializePlayer(boolean allowTunneling) { if (player == null) { Intent intent = getIntent(); String action = intent.getAction(); - Uri[] uris; - String[] extensions; + Uri[] uris = new Uri[0]; + String[] extensions = new String[0]; + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; + uris = new Uri[]{intent.getData()}; + extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } + uris = parseToUriList(uriStrings); extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); if (extensions == null) { extensions = new String[uriStrings.length]; } + } else if (ACTION_CHANNEL_LIST.equals(action)) { + channelUris = parseToUriList(uriStrings); + uris = new Uri[] { channelUris[0] }; + extensions = new String[] { null }; } else { showToast(getString(R.string.unexpected_intent_action, action)); finish(); return; } + if (!Util.checkCleartextTrafficPermitted(uris)) { showToast(R.string.error_cleartext_not_permitted); return; @@ -356,6 +419,18 @@ private void initializePlayer() { return; } + TrackSelection.Factory trackSelectionFactory; + String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); + if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { + trackSelectionFactory = new AdaptiveTrackSelection.Factory(); + } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { + trackSelectionFactory = new RandomTrackSelection.Factory(); + } else { + showToast(R.string.error_unrecognized_abr_algorithm); + finish(); + return; + } + DefaultDrmSessionManager drmSessionManager = null; if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); @@ -389,37 +464,12 @@ private void initializePlayer() { } } - TrackSelection.Factory trackSelectionFactory; - String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); - if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { - trackSelectionFactory = new AdaptiveTrackSelection.Factory(); - } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { - trackSelectionFactory = new RandomTrackSelection.Factory(); - } else { - showToast(R.string.error_unrecognized_abr_algorithm); - finish(); - return; - } - + boolean enableTunneling = intent.getBooleanExtra(ENABLE_TUNNELED_PLAYBACK, false) && allowTunneling; boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); - RenderersFactory renderersFactory = - ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); - - trackSelector = new DefaultTrackSelector(trackSelectionFactory); - trackSelector.setParameters(trackSelectorParameters); - lastSeenTrackGroupArray = null; - - player = - ExoPlayerFactory.newSimpleInstance( - /* context= */ this, renderersFactory, trackSelector, drmSessionManager); - player.addListener(new PlayerEventListener()); - player.setPlayWhenReady(startAutoPlay); - player.addAnalyticsListener(new EventLogger(trackSelector)); - playerView.setPlayer(player); - playerView.setPlaybackPreparer(this); - debugViewHelper = new DebugTextViewHelper(player, debugTextView); - debugViewHelper.start(); + + + createPlayer(trackSelectionFactory, drmSessionManager, enableTunneling, preferExtensionDecoders); MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { @@ -444,6 +494,10 @@ private void initializePlayer() { releaseAdsLoader(); } } + restartMediaSource(); + } + + private void restartMediaSource() { boolean haveStartPosition = startWindow != C.INDEX_UNSET; if (haveStartPosition) { player.seekTo(startWindow, startPosition); @@ -452,6 +506,76 @@ private void initializePlayer() { updateButtonVisibility(); } + private Uri[] parseToUriList(String[] uriStrings) { + Uri[] uris; + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + return uris; + } + + private void playUnencryptedUri(Uri uri) { + + Log.d("ExoPlayer", "change channel to " + uri); + mediaSource = buildMediaSource(uri); + clearStartPosition(); + + if (player == null) { + Intent intent = getIntent(); + boolean enableTunneling = intent.getBooleanExtra(ENABLE_TUNNELED_PLAYBACK, false); + + createPlayer(new AdaptiveTrackSelection.Factory(), null, enableTunneling, false); + } + restartMediaSource(); + } + + private void recreatePlayer() { + Intent intent = getIntent(); + boolean enableTunneling = intent.getBooleanExtra(ENABLE_TUNNELED_PLAYBACK, false); + + createPlayer(new AdaptiveTrackSelection.Factory(), null, enableTunneling, false); + restartMediaSource(); + } + + private void createPlayer(TrackSelection.Factory trackSelectionFactory, DefaultDrmSessionManager drmSessionManager, boolean enableTunneling, boolean preferExtensionDecoders) { + if (player != null) { + releasePlayer(); + } + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); + + trackSelector = new DefaultTrackSelector(trackSelectionFactory); + + // Get a builder with current parameters then set/clear tunnling based on the intent + // + Context context = getApplicationContext(); + int tunnelingSessionId = enableTunneling + ? C.generateAudioSessionIdV21(context) : C.AUDIO_SESSION_ID_UNSET; + + trackSelectorParameters = trackSelectorParameters.buildUpon() + .setTunnelingAudioSessionId(tunnelingSessionId) + .build(); + + // set the updated parameters for the trackSelector + trackSelector.setParameters(trackSelectorParameters); + lastSeenTrackGroupArray = null; + + player = + ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); + + player.addListener(new PlayerEventListener()); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); + + playerView.setPlayer(player); + playerView.setPlaybackPreparer(this); + + debugViewHelper = new DebugTextViewHelper(player, debugTextView); + debugViewHelper.start(); + + } + private MediaSource buildMediaSource(Uri uri) { return buildMediaSource(uri, null); } @@ -469,7 +593,7 @@ private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension case C.TYPE_SS: return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).setAllowChunklessPreparation(true).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: @@ -503,7 +627,6 @@ private void releasePlayer() { debugViewHelper = null; player.release(); player = null; - mediaSource = null; trackSelector = null; } if (adsLoader != null) { @@ -615,6 +738,10 @@ private static boolean isBehindLiveWindow(ExoPlaybackException e) { return false; } Throwable cause = e.getSourceException(); + return isCauseBehindLiveWindow(cause); + } + + private static boolean isCauseBehindLiveWindow(Throwable cause) { while (cause != null) { if (cause instanceof BehindLiveWindowException) { return true; @@ -624,6 +751,18 @@ private static boolean isBehindLiveWindow(ExoPlaybackException e) { return false; } + private static T getRootCauseOfType(Throwable exeception, Class exceptionType) { + T foundCause = null; + while (exeception != null && foundCause == null) { + if (exceptionType.isAssignableFrom(exeception.getClass())) { + foundCause = (T) exeception; + } + exeception = exeception.getCause(); + } + return foundCause; + } + + private class PlayerEventListener implements Player.EventListener { @Override @@ -636,10 +775,67 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { @Override public void onPlayerError(ExoPlaybackException e) { - if (isBehindLiveWindow(e)) { - clearStartPosition(); - initializePlayer(); - } else { + + boolean handled = false; + + switch (e.type) { + + /** + * Renderer exceptions occur for errors in writing samples, as well as codec initialization. + * Track selection may pick correct codecs but they may not play well togeather (for example + * tunneling mode not supported). Switching to an alternate decoder or changing attributes can + * work around this. + */ + case ExoPlaybackException.TYPE_RENDERER: + Exception renderException = e.getRendererException(); + if (renderException instanceof AudioSink.InitializationException) { + clearStartPosition(); + releasePlayer(); + initializePlayer(false); + handled = true; + } else if (renderException instanceof AudioSink.WriteException) { + AudioSink.WriteException writeException = (AudioSink.WriteException) renderException; + if (writeException.errorCode == android.media.AudioTrack.ERROR_DEAD_OBJECT) { + DefaultTrackSelector.Parameters trackSelectorParameters = trackSelector.getParameters(); + DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon(); + + TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); + for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { + builder.setRendererDisabled(i, true); + Log.d("ExoPlayer", "AudioSink.WriteException - disable audio track " + player.getRendererType(i) + " to recover"); + } + } + trackSelector.setParameters(builder); + + Log.d("ExoPlayer", "AudioSink.WriteException - reset player with prepare"); + player.prepare(mediaSource, true, true); + handled = true; + } + + } + break; + + case ExoPlaybackException.TYPE_SOURCE: + IOException sourceException = e.getSourceException(); + + BehindLiveWindowException liveWindowException = getRootCauseOfType(sourceException, BehindLiveWindowException.class); + if (liveWindowException != null) { + clearStartPosition(); + initializePlayer(true); + handled = true; + } else { + HttpDataSource.InvalidResponseCodeException invalidResponseCodeException = + getRootCauseOfType(sourceException, HttpDataSource.InvalidResponseCodeException.class); + + Log.d("ERROR", "Invalid HTTP response: " + invalidResponseCodeException.responseCode + + " headers: " + invalidResponseCodeException.headerFields + + " messaage: " + invalidResponseCodeException.responseMessage); + handled = true; + } + } + + if (! handled) { updateButtonVisibility(); showControls(); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 7245de01c64..0adf1297eb2 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -31,6 +31,7 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; +import android.widget.CheckBox; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; @@ -160,14 +161,18 @@ private void onSampleGroups(final List groups, boolean sawError) { @Override public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + + CheckBox tunnelMode = findViewById(R.id.tunnelMode); + Boolean tunnel = tunnelMode.isChecked(); + Sample sample = (Sample) view.getTag(); - startActivity( - sample.buildIntent( + String abrAlgorithm = isNonNullAndChecked(randomAbrMenuItem) + ? PlayerActivity.ABR_ALGORITHM_RANDOM + : PlayerActivity.ABR_ALGORITHM_DEFAULT; + Intent intent = sample.buildIntent( /* context= */ this, - isNonNullAndChecked(preferExtensionDecodersMenuItem), - isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT)); + isNonNullAndChecked(preferExtensionDecodersMenuItem), abrAlgorithm, tunnel); + startActivity(intent); return true; } @@ -534,10 +539,11 @@ public Sample(String name, DrmInfo drmInfo) { } public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { + Context context, boolean preferExtensionDecoders, String abrAlgorithm, Boolean tunnel) { Intent intent = new Intent(context, PlayerActivity.class); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); + intent.putExtra(PlayerActivity.ENABLE_TUNNELED_PLAYBACK, tunnel); if (drmInfo != null) { drmInfo.updateIntent(intent); } @@ -569,8 +575,8 @@ public UriSample( @Override public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) + Context context, boolean preferExtensionDecoders, String abrAlgorithm, Boolean tunnel) { + return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm, tunnel) .setData(uri) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) @@ -594,14 +600,14 @@ public PlaylistSample( @Override public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { + Context context, boolean preferExtensionDecoders, String abrAlgorithm, Boolean tunnel) { String[] uris = new String[children.length]; String[] extensions = new String[children.length]; for (int i = 0; i < children.length; i++) { uris[i] = children[i].uri.toString(); extensions[i] = children[i].extension; } - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) + return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm, tunnel) .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) .setAction(PlayerActivity.ACTION_VIEW_LIST); diff --git a/demos/main/src/main/res/layout/sample_chooser_activity.xml b/demos/main/src/main/res/layout/sample_chooser_activity.xml index 4d968c7497c..fd723da733d 100644 --- a/demos/main/src/main/res/layout/sample_chooser_activity.xml +++ b/demos/main/src/main/res/layout/sample_chooser_activity.xml @@ -18,8 +18,17 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + android:layout_width="match_parent" + android:layout_height="match_parent"/> diff --git a/demos/main/src/main/res/layout/sample_list_item.xml b/demos/main/src/main/res/layout/sample_list_item.xml index cdb0058688e..2f7b1b65c54 100644 --- a/demos/main/src/main/res/layout/sample_list_item.xml +++ b/demos/main/src/main/res/layout/sample_list_item.xml @@ -22,12 +22,12 @@ android:orientation="horizontal"> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceListItemSmall"/> + + + + + + + \ No newline at end of file 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 07a1438519c..fd30edc1db5 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 @@ -677,6 +677,7 @@ protected void onQueueInputBuffer(DecoderInputBuffer buffer) { } allowFirstBufferPositionDiscontinuity = false; } + Log.d("EXO-AUDIO", "queued audio input: currPos: " + currentPositionUs + " buffer.timeUs: " + buffer.timeUs + " buffer.pos: " + buffer.data.position()); lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); } @@ -715,6 +716,7 @@ protected boolean processOutputBuffer( bufferPresentationTimeUs = lastInputTimeUs; } + Log.d("EXO-AUDIO", "process audio output bufferIndex: " +bufferIndex + " positionUs: " + positionUs + " lastQueuedUs: " + lastInputTimeUs + " isDecodeOnly: "+isDecodeOnlyBuffer); if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // Discard output buffers from the passthrough (raw) decoder containing codec specific data. codec.releaseOutputBuffer(bufferIndex, false); @@ -730,6 +732,7 @@ protected boolean processOutputBuffer( try { if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + Log.d("EXO-AUDIO", "rendered audio output bufferIndex: " +bufferIndex + " positionUs: " + positionUs + " lastQueuedUs: " + lastInputTimeUs + " isDecodeOnly: "+isDecodeOnlyBuffer); codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.renderedOutputBufferCount++; return true; From 825b1321973e4d6a2cd843b7c9b8babd21cb0aa8 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 26 Aug 2019 11:04:45 -0700 Subject: [PATCH 216/219] Add logging for tunneling Add logging to show tunnel session id --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++++ .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 1 + 2 files changed, 5 insertions(+) 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 5894ac081ca..07b06c9a4cf 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 @@ -557,6 +557,10 @@ private void createPlayer(TrackSelection.Factory trackSelectionFactory, DefaultD .setTunnelingAudioSessionId(tunnelingSessionId) .build(); + if (enableTunneling) { + Log.d("EXO", "Enabling tunneling with sessionId: " + tunnelingSessionId); + } + // set the updated parameters for the trackSelector trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; 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 fd30edc1db5..8ffda72f426 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 @@ -932,6 +932,7 @@ private final class AudioSinkListener implements AudioSink.Listener { @Override public void onAudioSessionId(int audioSessionId) { + Log.d("EXO", "AudioSink.Listener - new session id: "+audioSessionId); eventDispatcher.audioSessionId(audioSessionId); MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId); } From 492fe8a2bd64c308e3a7b553055e4a6c4b59f392 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Thu, 29 Aug 2019 13:09:37 -0700 Subject: [PATCH 217/219] Add more logging to get at the issue. --- .../google/android/exoplayer2/audio/DefaultAudioSink.java | 3 +++ .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 2 +- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index be1b7d3d53d..53028c74844 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -513,6 +513,7 @@ private void initialize(long presentationTimeUs) throws InitializationException Assertions.checkNotNull(configuration) .buildAudioTrack(tunneling, audioAttributes, audioSessionId); int audioSessionId = audioTrack.getAudioSessionId(); + Log.d("EXO-AUDIO", "DefaultAudioTrack.initilize() built audioTrack - pts: " + presentationTimeUs + " sessionId: " +audioSessionId + " tunneling: " + tunneling); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { // The workaround creates an audio track with a two byte buffer on the same session, and @@ -758,6 +759,8 @@ private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throw bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } + Log.d("EXO-AUDIO", "Write audio output PTS - " + avSyncPresentationTimeUs + " written: " + bytesWritten + " buffer pos/limit: " + buffer.position() + "/" + buffer.limit()); + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); if (bytesWritten < 0) { 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 8ffda72f426..8597e0782ea 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 @@ -716,7 +716,7 @@ protected boolean processOutputBuffer( bufferPresentationTimeUs = lastInputTimeUs; } - Log.d("EXO-AUDIO", "process audio output bufferIndex: " +bufferIndex + " positionUs: " + positionUs + " lastQueuedUs: " + lastInputTimeUs + " isDecodeOnly: "+isDecodeOnlyBuffer); + Log.d("EXO-AUDIO", "Process audio outputBuffer PTS - " + bufferPresentationTimeUs + " bufferIndex: " +bufferIndex + " positionUs: " + positionUs + " lastQueuedUs: " + lastInputTimeUs + " isDecodeOnly: "+isDecodeOnlyBuffer); if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // Discard output buffers from the passthrough (raw) decoder containing codec specific data. codec.releaseOutputBuffer(bufferIndex, false); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index a8cf0f12e2f..d0ea2e0a248 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; @@ -1107,6 +1108,10 @@ private boolean feedInputBuffer() throws ExoPlaybackException { buffer.flip(); onQueueInputBuffer(buffer); + if (this instanceof MediaCodecAudioRenderer) { + Log.d("EXO-AUDIO", "Queued audio buffer PTS: " + buffer.timeUs + " index: " + inputIndex + " buffer size: " + buffer.data.limit()); + } + if (bufferEncrypted) { MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer, adaptiveReconfigurationBytes); From a212860af08adb3f0a0183b23f39c64aa4f2b809 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Wed, 4 Sep 2019 19:28:54 -0700 Subject: [PATCH 218/219] Add reams of logging --- .../android/exoplayer2/BaseRenderer.java | 5 +- .../audio/AudioTrackPositionTracker.java | 1 + .../exoplayer2/audio/DefaultAudioSink.java | 10 +- .../audio/MediaCodecAudioRenderer.java | 12 +- .../exoplayer2/extractor/ts/PesReader.java | 2 +- .../mediacodec/MediaCodecRenderer.java | 3 + .../source/MediaSourceEventListener.java | 5 + .../android/exoplayer2/util/EventLogger.java | 5 +- .../video/MediaCodecVideoRenderer.java | 11 ++ .../exoplayer2/ui/DebugTextViewHelper.java | 111 ++++++++++++++++-- 10 files changed, 147 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 1099b14bfcc..e17a1162bcf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -16,12 +16,15 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import java.io.IOException; /** @@ -337,7 +340,7 @@ protected int skipSource(long positionUs) { /** * Returns whether the upstream source is ready. */ - protected final boolean isSourceReady() { + protected boolean isSourceReady() { return hasReadStreamToEnd() ? streamIsFinal : stream.isReady(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index e87e49d2da6..11c70b39575 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 53028c74844..9fdffc7510f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -553,6 +553,8 @@ private void initialize(long presentationTimeUs) throws InitializationException public void play() { playing = true; if (isInitialized()) { + Log.d("EXO-AUDIO", "calling audioTrack.play()"); + audioTrackPositionTracker.start(); audioTrack.play(); } @@ -755,11 +757,13 @@ private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throw Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, avSyncPresentationTimeUs); + Log.d("EXO-AUDIO", "called writeNonBlockingWithAvSyncV21() PTS - " + avSyncPresentationTimeUs + " written: " + bytesWritten + + " size: " + bytesRemaining +" buffer pos/limit: " + buffer.position() + "/" + buffer.limit()); + } else { bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } - Log.d("EXO-AUDIO", "Write audio output PTS - " + avSyncPresentationTimeUs + " written: " + bytesWritten + " buffer pos/limit: " + buffer.position() + "/" + buffer.limit()); lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); @@ -938,7 +942,9 @@ private void setVolumeInternal() { @Override public void pause() { playing = false; - if (isInitialized() && audioTrackPositionTracker.pause()) { + boolean value = false; + if (isInitialized() && (value = audioTrackPositionTracker.pause())) { + Log.d("EXO-AUDIO", "calling audioTrack.pause() - audioTrackPositionTracker.pause() returned: " + value); audioTrack.pause(); } } 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 8597e0782ea..2940ea2327b 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 @@ -638,6 +638,15 @@ protected void onReset() { } } + @Override + protected boolean isSourceReady() { + boolean sourceReady = super.isSourceReady(); + if (! sourceReady) { + Log.d("EXO-AUDIO", "renderer not ready - readingPositionUs: " + getReadingPositionUs() + " currentPositionUs: " + currentPositionUs); + } + return sourceReady; + } + @Override public boolean isEnded() { return super.isEnded() && audioSink.isEnded(); @@ -677,7 +686,6 @@ protected void onQueueInputBuffer(DecoderInputBuffer buffer) { } allowFirstBufferPositionDiscontinuity = false; } - Log.d("EXO-AUDIO", "queued audio input: currPos: " + currentPositionUs + " buffer.timeUs: " + buffer.timeUs + " buffer.pos: " + buffer.data.position()); lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); } @@ -876,6 +884,8 @@ protected MediaFormat getMediaFormat( private void updateCurrentPosition() { long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + Log.d("EXO-AUDIO", "audioSink getCurrentPositionUs() - " + newCurrentPositionUs); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { currentPositionUs = allowPositionDiscontinuity diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index ff755f4ece2..49dd28b012e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -231,7 +231,7 @@ private void parseHeaderExtension() { // decode timestamp to the adjuster here so that in the case that this is the first to be // fed, the adjuster will be able to compute an offset to apply such that the adjusted // presentation timestamps of all future packets are non-negative. - timestampAdjuster.adjustTsTimestamp(dts); +// timestampAdjuster.adjustTsTimestamp(dts); seenFirstDts = true; } timeUs = timestampAdjuster.adjustTsTimestamp(pts); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index d0ea2e0a248..773c82e5277 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -48,6 +48,7 @@ import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1110,6 +1111,8 @@ private boolean feedInputBuffer() throws ExoPlaybackException { if (this instanceof MediaCodecAudioRenderer) { Log.d("EXO-AUDIO", "Queued audio buffer PTS: " + buffer.timeUs + " index: " + inputIndex + " buffer size: " + buffer.data.limit()); + } else if (this instanceof MediaCodecVideoRenderer) { + Log.d("EXO-VIDEO", "Queued video buffer PTS: " + buffer.timeUs + " index: " + inputIndex + " buffer size: " + buffer.data.limit()); } if (bufferEncrypted) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 233e19b29c8..e7bfe872b1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -85,6 +85,11 @@ public LoadEventInfo( this.loadDurationMs = loadDurationMs; this.bytesLoaded = bytesLoaded; } + + @Override + public String toString() { + return "load - URI: " + uri + " loaded: " + bytesLoaded + " duration: " + loadDurationMs; + } } /** Descriptor for data being loaded or selected by a media source. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index bb3dc8b83ab..4ded1eebf13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -339,7 +339,7 @@ public void onMediaPeriodReleased(EventTime eventTime) { @Override public void onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - // Do nothing. + logd(eventTime,"loadStarted", loadEventInfo.toString()); } @Override @@ -355,13 +355,14 @@ public void onLoadError( @Override public void onLoadCanceled( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + logd(eventTime,"loadCanceled", loadEventInfo.toString()); // Do nothing. } @Override public void onLoadCompleted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - // Do nothing. + logd(eventTime,"loadCompleted", loadEventInfo.toString()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index b5a935c15f9..684913c99be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -469,6 +469,15 @@ protected void onReset() { } } + @Override + protected boolean isSourceReady() { + boolean sourceReady = super.isSourceReady(); + if (! sourceReady) { + Log.d("EXO-VIDEO", "renderer not ready - readingPositionUs: " + getReadingPositionUs() + " lastInputTimeUs: " + lastInputTimeUs + " buffersInCodecCount:" + buffersInCodecCount); + } + return sourceReady; + } + @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { @@ -825,6 +834,7 @@ protected void onProcessedTunneledBuffer(long presentationTimeUs) { } maybeNotifyVideoSizeChanged(); maybeNotifyRenderedFirstFrame(); + Log.d("EXO-VIDEO", "processed tunneled buffer - PTS: " + presentationTimeUs + " buffersInCodec: " + buffersInCodecCount); onProcessedOutputBuffer(presentationTimeUs); } @@ -1603,6 +1613,7 @@ public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, // Stale event. return; } + Log.d("EXO-VIDEO", "Tunneled frame rendered, PTS - " + presentationTimeUs); onProcessedTunneledBuffer(presentationTimeUs); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index da2081db31d..07f5b803f53 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -17,19 +17,33 @@ import android.annotation.SuppressLint; import android.os.Looper; +import android.util.Pair; import android.widget.TextView; + +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Log; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Locale; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public class DebugTextViewHelper implements Player.EventListener, Runnable { +public class DebugTextViewHelper implements AnalyticsListener, Runnable { private static final int REFRESH_INTERVAL_MS = 1000; @@ -37,6 +51,12 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { private final TextView textView; private boolean started; + private long bitrateEstimate; + + private long networkActiveTime; + private long totalBytesLoaded; + private long startTime; + private Format loadingVideoFormat; /** * @param player The {@link SimpleExoPlayer} from which debug information should be obtained. Only @@ -59,7 +79,10 @@ public final void start() { return; } started = true; - player.addListener(this); + networkActiveTime = 0; + totalBytesLoaded = 0; + startTime = Clock.DEFAULT.elapsedRealtime(); + player.addAnalyticsListener(this); updateAndPost(); } @@ -72,22 +95,56 @@ public final void stop() { return; } started = false; - player.removeListener(this); + player.removeAnalyticsListener(this); textView.removeCallbacks(this); } - // Player.EventListener implementation. + // Analytics implementation. + @Override + public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) { + updateAndPost(); + } @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPositionDiscontinuity(EventTime eventTime, int reason) { updateAndPost(); } @Override - public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + public void onTimelineChanged(EventTime eventTime, int reason) { + Timeline timeline = eventTime.timeline; + + if (timeline.getWindowCount() > 0) { + Timeline.Window window = new Timeline.Window(); + timeline.getWindow(0, window); + DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.S"); + Date start = new Date(window.windowStartTimeMs); + String epochDate = format.format(start); + + Log.d("DEBUG", "TimeLine changed: first window start: " + epochDate + " position: " + window.positionInFirstPeriodUs); + } updateAndPost(); } + @Override + public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + this.bitrateEstimate = bitrateEstimate; + this.networkActiveTime += totalLoadTimeMs; + this.totalBytesLoaded += totalBytesLoaded; + } + + @Override + public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + Log.d("DebugText", "tracksChanegd: " + trackSelections); + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) { + if (mediaLoadData.trackType != C.TRACK_TYPE_AUDIO) { + this.loadingVideoFormat = mediaLoadData.trackFormat; + } + } + // Runnable implementation. @Override @@ -129,16 +186,45 @@ protected String getPlayerStateString() { playbackStateString = "unknown"; break; } - return String.format( - "playWhenReady:%s playbackState:%s window:%s", - player.getPlayWhenReady(), playbackStateString, player.getCurrentWindowIndex()); + + long bandwidthUsed = networkActiveTime == 0 ? 0 : totalBytesLoaded / networkActiveTime; + long timeSinceStart = Clock.DEFAULT.elapsedRealtime() - startTime; + double percentNet = ((double) networkActiveTime / (double) timeSinceStart) * 100.0; + + return String.format(Locale.getDefault(),"playWhenReady:%s bw:%d (%d KBps) totalBw: %d netu: %3.2f window:%s cp:%s playbackState:%s", + player.getPlayWhenReady(), bitrateEstimate, bitrateEstimate / 8000, bandwidthUsed, percentNet, player.getCurrentWindowIndex(), getPositionString(), playbackStateString); + } + + protected String getPositionString() { + long position = player.getCurrentPosition(); + String time = ""; + + Timeline timeline = player.getCurrentTimeline(); + if (timeline != Timeline.EMPTY) { + int windowIndex = player.getCurrentWindowIndex(); + Timeline.Window currentWindow = new Timeline.Window(); + Pair periodPosition = timeline.getPeriodPosition(currentWindow, new Timeline.Period(), windowIndex, position); + long absTime; + DateFormat format; + if (currentWindow.windowStartTimeMs == C.TIME_UNSET) { + format = new SimpleDateFormat("HH:mm:ss.S Z", Locale.getDefault()); + absTime = position; + } else { + format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.S Z", Locale.getDefault()); + absTime = currentWindow.windowStartTimeMs + position; + } + Date currentMediaTime = new Date(absTime); + time = format.format(currentMediaTime); + } + + return time + " (" + position + ")"; } /** Returns a string containing video debugging information. */ protected String getVideoString() { Format format = player.getVideoFormat(); DecoderCounters decoderCounters = player.getVideoDecoderCounters(); - if (format == null || decoderCounters == null) { + if (format == null || decoderCounters == null || loadingVideoFormat == null) { return ""; } return "\n" @@ -150,6 +236,8 @@ protected String getVideoString() { + "x" + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) + + " bitrate:"+format.bitrate+" " + + " loading (bitrate:"+ loadingVideoFormat.bitrate+", id:"+ loadingVideoFormat.id+") " + getDecoderCountersBufferCountString(decoderCounters) + ")"; } @@ -178,7 +266,8 @@ private static String getDecoderCountersBufferCountString(DecoderCounters counte return ""; } counters.ensureUpdated(); - return " sib:" + counters.skippedInputBufferCount + return " qib:" + counters.inputBufferCount + + " sib:" + counters.skippedInputBufferCount + " sb:" + counters.skippedOutputBufferCount + " rb:" + counters.renderedOutputBufferCount + " db:" + counters.droppedBufferCount From 066e66e08349a9ff28662dd485678c3a4a5b4e31 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Thu, 5 Sep 2019 13:47:55 -0700 Subject: [PATCH 219/219] Revert non logging changes, add workaround in MediaCodecVideoRenderer See issue #6366 for more blow by blow of the issue. The work around is to keep the video renderer ready, even if it is out of upstream samples, as long as it has samples in the codec that will eventually hit their render time. A more "correct fix" might be to understand and implement the logic for AudioTrack.write() when the audio track is paused. --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 6 +++++- .../google/android/exoplayer2/extractor/ts/PesReader.java | 2 +- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) 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 65a6866a9fb..99e91698e43 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 @@ -578,7 +578,11 @@ private void doSomeWork() throws ExoPlaybackException, IOException { // ready if it needs the next sample stream. This is necessary to avoid getting stuck if // tracks in the current period have uneven durations. See: // https://github.com/google/ExoPlayer/issues/1874 - boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded() + boolean oneRendererReady = renderer.isReady(); + if (! oneRendererReady) { + Log.d("EXO-STUCK", "renderer not ready - track type: " + renderer.getTrackType()); + } + boolean rendererReadyOrEnded = oneRendererReady || renderer.isEnded() || rendererWaitingForNextStream(renderer); if (!rendererReadyOrEnded) { renderer.maybeThrowStreamError(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 49dd28b012e..ff755f4ece2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -231,7 +231,7 @@ private void parseHeaderExtension() { // decode timestamp to the adjuster here so that in the case that this is the first to be // fed, the adjuster will be able to compute an offset to apply such that the adjusted // presentation timestamps of all future packets are non-negative. -// timestampAdjuster.adjustTsTimestamp(dts); + timestampAdjuster.adjustTsTimestamp(dts); seenFirstDts = true; } timeUs = timestampAdjuster.adjustTsTimestamp(pts); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 684913c99be..dde12a34480 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -405,7 +405,11 @@ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlayb @Override public boolean isReady() { - if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) +// if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) + // This "works around" the problem, even if it is not time to render a frame, and we don't have samples + // (because the entire LoadControl limited buffered sample queue is in the decoder), stay 'ready' to render the frame + // when it is due. + if ((super.isReady() || buffersInCodecCount > 0) && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET;