From dcb8417a3c6a18e636be07c44d21eddf98208639 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 17 Apr 2019 09:30:25 +0100 Subject: [PATCH 001/424] 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 5acefd6f93..7ff43ceacd 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 final class DownloadRequest implements Parcelable { 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 final class DownloadRequest implements Parcelable { 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/424] 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 8e5a6d2a9b..15deb8ea47 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 @@ import java.util.concurrent.CopyOnWriteArrayList; 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/424] 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 d7ab4201a5..fc1518e5c3 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 @@ import java.util.List; *

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 final class DefaultDownloadIndex implements DownloadIndex { 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 final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * 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 final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * 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 final class DefaultDownloadIndex implements DownloadIndex { } } - /** - * 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 90d0fa1b51..3de1b7b212 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 @@ package com.google.android.exoplayer2.offline; 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 03c33b6aad..fdb3ca1840 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 android.os.Message; 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 @@ public final class DownloadManager { 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 final class 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 final class DownloadManager { */ public DownloadManager( Context context, - DefaultDownloadIndex downloadIndex, + WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory, int maxSimultaneousDownloads, int minRetryCount, @@ -651,7 +650,7 @@ public final class DownloadManager { } else { downloadIndex.setManualStopReason(manualStopReason); } - } catch (DatabaseIOException e) { + } catch (IOException e) { Log.e(TAG, "setManualStopReason failed", e); } } @@ -734,7 +733,7 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 0000000000..24f4421bc4 --- /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/424] 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 765244ac1a..182701ec34 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 4db715f53e..c65ad0bc67 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 @@ public final class Mp3Extractor implements Extractor { */ 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/424] 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 c80fb26124..a91bbbd981 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/424] 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 34282fc389..4a7a810314 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 class DownloadTracker { } } } - 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 class DownloadTracker { // 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/424] 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 91349e9284..12905f908c 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 @@ public interface Cache { 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 dbec4b78fc..6ebfe01df4 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 @@ import com.google.android.exoplayer2.C; */ 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 492b98a0de..7ac80325a5 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 com.google.android.exoplayer2.upstream.cache; /* 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 084c02b11b..027172e090 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + " 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * * @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 7dbcd4a922..1e8cf1517d 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 class CacheSpan implements Comparable { * @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 e244163bc8..7abb9b3896 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 @@ import java.util.TreeSet; } /** - * 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 aa40c1d2fd..44a735f144 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 final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar this.leastRecentlyUsed = new TreeSet<>(this); } + @Override + public boolean requiresCacheSpanTouches() { + return true; + } + @Override public void onCacheInitialized() { // Do nothing. @@ -68,12 +73,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar @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 b0c8c7e087..da89dc1cb3 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 @@ package com.google.android.exoplayer2.upstream.cache; */ 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 14f659855b..b31d3b66f3 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 final class SimpleCache implements Cache { this.fileIndex = fileIndex; listeners = new HashMap<>(); random = new Random(); + touchCacheSpans = evictor.requiresCacheSpanTouches(); uid = UID_UNSET; // Start cache initialization. @@ -408,23 +410,26 @@ public final class SimpleCache implements Cache { // 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 final class SimpleCache implements Cache { 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 final class SimpleCache implements Cache { 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 @@ public final class SimpleCache implements Cache { 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 7235830019..7d9f0c9ff1 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 @@ import java.util.regex.Pattern; */ @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 @@ import java.util.regex.Pattern; * @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 @@ import java.util.regex.Pattern; } 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 @@ import java.util.regex.Pattern; * @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 bebcf0ec12..cee5703ff8 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 class CachedContentIndexTest { 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 class CachedContentIndexTest { 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 5efdf36191..b00ee73f0f 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 @@ public final class CachedRegionTrackerTest { } 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 028937dc5a..39be9fbcd8 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 @@ import org.junit.runner.RunWith; 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 class SimpleCacheSpanTest { 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 @@ public class SimpleCacheSpanTest { 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 @@ public class SimpleCacheSpanTest { 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/424] 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 fdb3ca1840..915f375027 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 @@ public final class 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. */ public DownloadManager( @@ -204,7 +204,7 @@ public final class 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/424] 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 9a4e5925ee..ca20c769dc 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 @@ public class DefaultDownloaderFactory implements DownloaderFactory { 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 915f375027..df958f8691 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 @@ public final class DownloadManager { 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/424] [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 952e15aad6..d5da9a011d 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 class LibvpxVideoRenderer extends BaseRenderer { 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/424] 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 2ce9b8bdbe..e87e49d2da 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 @@ import java.lang.reflect.Method; 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/424] 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 97d330cdaa..095739271e 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 androidx.test.ext.junit.runners.AndroidJUnit4; 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 class HlsMasterPlaylistParserTest { .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/424] 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 f23248952c..140347bd91 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 @@ public class DownloadManagerTest { } } - 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 76356cf3a8..0dce24bf1d 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.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; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.RobolectricUtil; @@ -100,8 +101,8 @@ public class DownloadManagerDashTest { } @After - public void tearDown() throws Exception { - downloadManager.release(); + public void tearDown() { + runOnMainThread(() -> downloadManager.release()); Util.recursiveDelete(tempFolder); dummyMainThread.release(); } @@ -129,10 +130,11 @@ public class DownloadManagerDashTest { // 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 @@ public class DownloadManagerDashTest { } 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 @@ public class DownloadManagerDashTest { 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 b74e539fd6..9d6223b8b1 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 final class TestDownloadManagerListener implements DownloadManager.Listen 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/424] 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 | 44 +++++++++---------- .../DefaultTrackSelectorTest.java | 36 +++++++-------- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 182701ec34..015b348f68 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 f25f1a979c..3200e40495 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - /** - * Returns whether a {@link Format} does not define a language. - * - * @param format The {@link Format}. - * @return Whether the {@link Format} does not define a language. - */ + /** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */ protected static boolean formatHasNoLanguage(Format format) { - return TextUtils.isEmpty(format.language) - || TextUtils.equals(format.language, C.LANGUAGE_UNDETERMINED); + return stringDefinesNoLanguage(format.language); + } + + /** + * Returns whether the given string does not define a language. + * + * @param language The string. + * @return Whether the given string does not define a language. + */ + 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 3091e46456..83fe34db97 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 final class DefaultTrackSelectorTest { 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 final class DefaultTrackSelectorTest { .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 final class DefaultTrackSelectorTest { 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 final class DefaultTrackSelectorTest { 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/424] 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 1d2068e5f7..33161b4121 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 84a8a4087c..7089d4d731 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 4a7a810314..a860d96e43 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 class DownloadTracker { 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 @@ public class DownloadTracker { } 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 573426df2e..4dc463ff81 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 baf925acbd..ad45f61d98 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 ee3358d21a..ffecdcd16f 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 9a247c3f8f..06a5888404 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 6c4bfa469a..50acd6c040 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 a86eedc2d4..c6f5a216ce 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 eddd364370..db2e073c8a 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 e7c7fce164..ca734c3657 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 4b2ba26ca2..02b68b831d 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 deb9f24dce..68ff8cc977 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 c206a94d6d..6922d6a787 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { */ 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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 @@ public abstract class DownloadService extends Service { } } + 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 c7e68f548a..f6981a2220 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 99619bf750..8e9696af70 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 ba3b4ab65d..a2e81fb304 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 9c47f3684d..49446b25de 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 0e1c8a1268..dd5cfa64a7 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 ab78e6673f..bdc26d5c19 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 44459ea272..a3859a9e48 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/424] 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 0a37fe3a80..51996ed284 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 final class ActionFileUpgradeUtil { 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 final class ActionFileUpgradeUtil { 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 fc1518e5c3..a2caff3ff1 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 final class DefaultDownloadIndex implements WritableDownloadIndex { } @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 final class DefaultDownloadIndex implements WritableDownloadIndex { } @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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { 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 b29abde24b..343b9d6a49 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 final class Download { * #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 final class Download { 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 final class Download { 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 df958f8691..497e3476af 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 @@ package com.google.android.exoplayer2.offline; 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_QUEUED; 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 @@ public final class DownloadManager { 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 final class DownloadManager { 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 final class DownloadManager { } /** - * 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 final class DownloadManager { * @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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } /* 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 @@ public final class DownloadManager { download.request.copyWithMergedRequest(request), state, FAILURE_REASON_NONE, - manualStopReason, + stopReason, startTimeMs, /* updateTimeMs= */ nowMs, download.counters); @@ -846,7 +843,7 @@ public final class DownloadManager { download.request, state, FAILURE_REASON_NONE, - download.manualStopReason, + download.stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -882,21 +879,21 @@ public final class DownloadManager { // 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 final class DownloadManager { download.request, state, state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - manualStopReason, + stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), download.counters); @@ -934,8 +931,8 @@ public final class DownloadManager { } } - public void setManualStopReason(int manualStopReason) { - this.manualStopReason = manualStopReason; + public void setStopReason(int stopReason) { + this.stopReason = stopReason; updateStopState(); } @@ -981,7 +978,7 @@ public final class DownloadManager { } 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 6922d6a787..fa74afacb3 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { } /** - * 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 abstract class DownloadService extends Service { } /** - * 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 24f4421bc4..2306363cf5 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 5bd1f34ed4..a426a7488b 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 class DefaultDownloadIndexTest { .setDownloadedBytes(200) .setTotalBytes(400) .setFailureReason(Download.FAILURE_REASON_UNKNOWN) - .setManualStopReason(0x12345678) + .setStopReason(0x12345678) .setStartTimeMs(10) .setUpdateTimeMs(20) .setStreamKeys( @@ -204,23 +204,22 @@ public class DefaultDownloadIndexTest { } @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 class DefaultDownloadIndexTest { .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 class DefaultDownloadIndexTest { .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 @@ public class DefaultDownloadIndexTest { 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 2e14caa5bd..b5d84fa4bc 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 @@ class DownloadBuilder { return this; } - public DownloadBuilder setManualStopReason(int manualStopReason) { - this.manualStopReason = manualStopReason; + public DownloadBuilder setStopReason(int stopReason) { + this.stopReason = stopReason; return this; } @@ -156,6 +156,6 @@ class DownloadBuilder { 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 140347bd91..2909bfd779 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 @@ public class DownloadManagerTest { 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/424] Rename start/stopDownloads to resume/pauseDownloads PiperOrigin-RevId: 244216620 --- .../exoplayer2/offline/DownloadManager.java | 34 ++-- .../exoplayer2/offline/DownloadService.java | 146 ++++++++++-------- .../offline/DownloadManagerTest.java | 6 +- .../dash/offline/DownloadManagerDashTest.java | 2 +- .../dash/offline/DownloadServiceDashTest.java | 2 +- 5 files changed, 105 insertions(+), 85 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 497e3476af..c34a5e233a 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * 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 @@ public final class DownloadManager { // 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 @@ public final class DownloadManager { // 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 final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { } 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 fa74afacb3..9de6c748fb 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 @@ -277,6 +277,32 @@ public abstract class DownloadService extends Service { return getIntent(context, clazz, ACTION_REMOVE, foreground).putExtra(KEY_CONTENT_ID, id); } + /** + * 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 foreground Whether this intent will be used to start the service in the foreground. + * @return Created Intent. + */ + public static Intent buildResumeDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_RESUME, foreground); + } + + /** + * 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 buildPauseDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_PAUSE, foreground); + } + /** * 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}. @@ -299,32 +325,6 @@ public abstract class DownloadService extends Service { .putExtra(KEY_STOP_REASON, stopReason); } - /** - * 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. * @@ -342,6 +342,26 @@ public abstract class DownloadService extends Service { 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. * @@ -356,6 +376,32 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * 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 foreground Whether the service is started in the 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 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 sendPauseDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildPauseDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + /** * 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}. @@ -376,32 +422,6 @@ public abstract class DownloadService extends Service { 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); - } - /** * Starts a download service to resume any ongoing downloads. * @@ -438,7 +458,7 @@ public abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 2909bfd779..b1864165b3 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 class DownloadManagerTest { runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving(); runner2.postDownloadRequest(); - runOnMainThread(() -> downloadManager.stopDownloads()); + runOnMainThread(() -> downloadManager.pauseDownloads()); runner1.getTask().assertStopped(); @@ -386,7 +386,7 @@ public class DownloadManagerTest { // 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 @@ public class DownloadManagerTest { 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 0dce24bf1d..02af54836c 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 @@ public class DownloadManagerDashTest { 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 a35b6d1ea4..b2b42c987e 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 class DownloadServiceDashTest { 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/424] 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 446184e56b..2c9cd43d1e 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 @@ public class DemoApplication extends Application { 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 c34a5e233a..f914c861f9 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 android.os.Message; 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 @@ public final class DownloadManager { @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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { // 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 final class 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 final class 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 final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } 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 b1864165b3..17328248c6 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 class DownloadManagerTest { 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 @@ public class DownloadManagerTest { () -> { 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 02af54836c..9fc9834e1d 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.DownloadManager; 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 @@ public class DownloadManagerDashTest { 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 b2b42c987e..57e7b8de5f 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.DownloadRequest; 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 class DownloadServiceDashTest { 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/424] 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 a2caff3ff1..30297f19ce 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.database.sqlite.SQLiteException; 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 com.google.android.exoplayer2.util.Util; 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}. * - * @param databaseProvider A DatabaseProvider which provides the database which will be used to - * store DownloadStatus table. + *

    Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code + * name=""}. + * + *

    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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } 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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { 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 a426a7488b..73c73b6647 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 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DefaultDownloadIndexTest { + private static final String EMPTY_NAME = "singleton"; + private ExoDatabaseProvider databaseProvider; private DefaultDownloadIndex downloadIndex; @@ -170,14 +171,12 @@ public class DefaultDownloadIndexTest { @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 class DefaultDownloadIndexTest { 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/424] 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 | 29 ++- .../android/exoplayer2/offline/Download.java | 67 +++--- .../exoplayer2/offline/DownloadManager.java | 107 ++++++--- .../exoplayer2/offline/DownloadProgress.java | 28 +++ .../exoplayer2/offline/Downloader.java | 48 ++-- .../offline/ProgressiveDownloader.java | 46 ++-- .../exoplayer2/offline/SegmentDownloader.java | 208 +++++++++--------- .../exoplayer2/upstream/cache/CacheUtil.java | 137 ++++++------ .../offline/DefaultDownloadIndexTest.java | 14 +- .../exoplayer2/offline/DownloadBuilder.java | 76 ++++--- .../offline/DownloadManagerTest.java | 44 ++-- .../upstream/cache/CacheDataSourceTest.java | 8 +- .../upstream/cache/CacheUtilTest.java | 136 +++++++----- .../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, 593 insertions(+), 463 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 51996ed284..b601874f8d 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 @@ package com.google.android.exoplayer2.offline; 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 final class ActionFileUpgradeUtil { 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 30297f19ce..6838c24628 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 android.text.TextUtils; 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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_FAILURE_REASON, download.failureReason); - 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); + 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); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); @@ -337,18 +336,18 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { 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 343b9d6a49..9f6b473208 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 @@ package com.google.android.exoplayer2.offline; 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 final class Download { /** 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 final class Download { } /** 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 f914c861f9..d4df5cd18b 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 androidx.annotation.Nullable; 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } + 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { // 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 final class DownloadManager { 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 final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 final class DownloadManager { // 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 final class DownloadManager { 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 final class DownloadManager { } 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(); } } - private int getRetryDelayMillis(int errorCount) { + @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 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 0000000000..9d946daa28 --- /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 39f562ac19..fa10d5842b 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 9794b19b62..17f4047bc0 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.Cache; 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 final class ProgressiveDownloader implements Downloader { 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 final class ProgressiveDownloader implements Downloader { new byte[BUFFER_SIZE_BYTES], priorityTaskManager, C.PRIORITY_DOWNLOAD, - cachingCounters, + progressListener == null ? null : new ProgressForwarder(progressListener), isCanceled, /* enableEOFException= */ true); } finally { @@ -91,28 +89,26 @@ public final class ProgressiveDownloader implements Downloader { isCanceled.set(true); } - @Override - public long getDownloadedBytes() { - return cachingCounters.totalCachedBytes(); - } - - @Override - public long getTotalBytes() { - return cachingCounters.contentLength; - } - - @Override - public float getDownloadPercentage() { - return cachingCounters.percentage; - } - - @Override - public CachingCounters getCounters() { - return cachingCounters; - } - @Override public void remove() { CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } + + private static final class ProgressForwarder implements CacheUtil.ProgressListener { + + private final ProgressListener progessListener; + + public ProgressForwarder(ProgressListener progressListener) { + this.progessListener = progressListener; + } + + @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 4dbae47775..1643812ece 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 @@ package com.google.android.exoplayer2.offline; 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.Cache; 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 abstract class SegmentDownloader> impleme 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 abstract class SegmentDownloader> impleme 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 abstract class SegmentDownloader> impleme * @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 abstract class SegmentDownloader> impleme 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 abstract class SegmentDownloader> impleme * @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 @@ public abstract class SegmentDownloader> impleme /* 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 f715da118b..219d736835 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 @@ package com.google.android.exoplayer2.upstream.cache; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * 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 final class CacheUtil { } /** - * 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 final class CacheUtil { * @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 final class CacheUtil { 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 final class CacheUtil { new byte[DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - counters, + progressListener, isCanceled, /* enableEOFException= */ false); } @@ -176,7 +157,7 @@ public final class CacheUtil { * @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 final class CacheUtil { 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 final class CacheUtil { buffer, priorityTaskManager, priority, - counters, + progressNotifier, isCanceled); if (read < blockLength) { // Reached to the end of the data. @@ -261,7 +241,7 @@ public final class CacheUtil { * @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 @@ public final class CacheUtil { 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 @@ public final class CacheUtil { 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 @@ public final class CacheUtil { ? (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 @@ public final class CacheUtil { 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 73c73b6647..f163e8d206 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 class DefaultDownloadIndexTest { .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 @@ public class DefaultDownloadIndexTest { 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 b5d84fa4bc..f901b00f53 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 @@ package com.google.android.exoplayer2.offline; 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 @@ import java.util.List; * 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; - @Nullable private String cacheKey; - private int state; - private int failureReason; - private int stopReason; - private long startTimeMs; - private long updateTimeMs; private List streamKeys; + @Nullable private String cacheKey; private byte[] customMetadata; - DownloadBuilder(String id) { - this(id, "type", Uri.parse("uri"), /* cacheKey= */ null, new byte[0], Collections.emptyList()); + private int state; + private long startTimeMs; + private long updateTimeMs; + private long contentLength; + private int stopReason; + private int failureReason; + + /* 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.cacheKey = cacheKey; - this.state = Download.STATE_QUEUED; - this.failureReason = Download.FAILURE_REASON_NONE; - this.startTimeMs = (long) 0; - this.updateTimeMs = (long) 0; this.streamKeys = streamKeys; + this.cacheKey = cacheKey; this.customMetadata = customMetadata; - this.counters = new CachingCounters(); + this.state = Download.STATE_QUEUED; + this.contentLength = C.LENGTH_UNSET; + this.failureReason = Download.FAILURE_REASON_NONE; + this.progress = new DownloadProgress(); } public DownloadBuilder setId(String id) { @@ -107,18 +116,18 @@ class DownloadBuilder { 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 @@ class DownloadBuilder { 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 17328248c6..5798e9df8c 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 static com.google.common.truth.Truth.assertThat; 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.DummyMainThread.TestRunnable; 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 class DownloadManagerTest { 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 @@ public class DownloadManagerTest { 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 @@ public class DownloadManagerTest { 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 @@ public class DownloadManagerTest { return this; } - private FakeDownloader assertStartCount(int count) throws InterruptedException { + private FakeDownloader assertStartCount(int count) { assertThat(startCount).isEqualTo(count); return this; } @@ -823,34 +825,14 @@ public class DownloadManagerTest { 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 4005edc3a6..956a5fc283 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 final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -392,7 +392,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -416,7 +416,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. @@ -452,7 +452,7 @@ public final class CacheDataSourceTest { 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 ba06862385..9a449b2ebd 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 com.google.common.truth.Truth.assertThat; 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.FakeDataSource; 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 final class CacheUtilTest { } @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 final class CacheUtilTest { } @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 final class CacheUtilTest { } @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); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec( + Uri.parse("test"), + /* absoluteStreamPosition= */ 100, + /* length= */ C.LENGTH_UNSET, + /* key= */ null), + mockCache, + /* cacheKeyFactory= */ null); - assertCounters(counters, 200, 0, 900); + assertThat(contentLengthAndBytesCached.first).isEqualTo(900); + assertThat(contentLengthAndBytesCached.second).isEqualTo(200); } @Test @@ -208,7 +212,7 @@ public final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 100); + counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @@ -223,7 +227,8 @@ public final class CacheUtilTest { 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 final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -249,7 +254,7 @@ public final class CacheUtilTest { 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 final class CacheUtilTest { 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 final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -291,7 +297,7 @@ public final class CacheUtilTest { 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 final class CacheUtilTest { 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 final class CacheUtilTest { 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 final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 300, 300); + counters.assertValues(0, 300, 300); assertCachedData(cache, fakeDataSet); } @@ -369,7 +375,7 @@ public final class CacheUtilTest { 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 final class CacheUtilTest { 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 5636c73491..2754a3341a 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 @@ import java.util.List; *

    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 @@ import java.util.List;
      *     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 9eacd28f8d..b3a6b8271b 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 class DashDownloaderTest {
         tempFolder =
             Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
         cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
    +    progressListener = new ProgressListener();
       }
     
       @After
    @@ -77,7 +79,7 @@ public class DashDownloaderTest {
       }
     
       @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 class DashDownloaderTest {
                 .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 class DashDownloaderTest {
                 .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 class DashDownloaderTest {
     
         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 class DashDownloaderTest {
                 .setRandomData("period_2_segment_3", 3);
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
    -    dashDownloader.download();
    +    dashDownloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -186,7 +188,7 @@ public class DashDownloaderTest {
     
         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 class DashDownloaderTest {
     
         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 class DashDownloaderTest {
     
         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 class DashDownloaderTest {
                 .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 class DashDownloaderTest {
     
         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 class DashDownloaderTest {
     
         DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
         try {
    -      dashDownloader.download();
    +      dashDownloader.download(progressListener);
           fail();
         } catch (DownloadException e) {
           // Expected.
    @@ -339,4 +340,17 @@ public class DashDownloaderTest {
         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 8e744f9a77..6e6d0afd49 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 @@ import java.util.List;
      * 

    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 @@ import java.util.List;
      *         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 b92953c3b5..7d77a78316 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 class HlsDownloaderTest {
         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 class HlsDownloaderTest {
       }
     
       @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 class HlsDownloaderTest {
       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 class HlsDownloaderTest {
             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 class HlsDownloaderTest {
             .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 class HlsDownloaderTest {
             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 class HlsDownloaderTest {
       @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 class HlsDownloaderTest {
                 .setRandomData("fileSequence2.ts", 12);
     
         HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys());
    -    downloader.download();
    +    downloader.download(progressListener);
         assertCachedData(cache, fakeDataSet);
       }
     
    @@ -222,4 +222,18 @@ public class HlsDownloaderTest {
         }
         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 18820ca49c..1331fe4617 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 @@ import java.util.List;
      * 

    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 @@ import java.util.List;
      *         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 b26b8eaac4..178cd44dd3 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 final class DownloadNotificationHelper {
             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 67c840e681..f5af2472c9 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 final class DashDownloadTest {
       @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/424] Clean up database tables for launch
    
    PiperOrigin-RevId: 244267255
    ---
     .../offline/DefaultDownloadIndex.java         | 131 ++++++++----------
     .../cache/CacheFileMetadataIndex.java         |   5 +-
     .../upstream/cache/CachedContentIndex.java    |  13 +-
     .../offline/DefaultDownloadIndexTest.java     |   2 +-
     4 files changed, 62 insertions(+), 89 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 6838c24628..252c058b88 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.database.sqlite.SQLiteException;
     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
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_STOP_FLAGS
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_NOT_MET_REQUIREMENTS
    -          + " INTEGER NOT NULL,"
    -          + COLUMN_STOP_REASON
    -          + " INTEGER NOT NULL,"
               + COLUMN_START_TIME_MS
               + " INTEGER NOT NULL,"
               + COLUMN_UPDATE_TIME_MS
               + " INTEGER NOT NULL,"
    -          + COLUMN_STREAM_KEYS
    -          + " TEXT NOT NULL,"
    -          + COLUMN_DATA
    -          + " BLOB NOT NULL)";
    +          + COLUMN_CONTENT_LENGTH
    +          + " INTEGER NOT NULL,"
    +          + COLUMN_STOP_REASON
    +          + " INTEGER NOT NULL,"
    +          + COLUMN_FAILURE_REASON
    +          + " INTEGER 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 final class DefaultDownloadIndex implements WritableDownloadIndex {
        *     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 final class DefaultDownloadIndex implements WritableDownloadIndex {
         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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
         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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
             } 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 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
       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 027172e090..2a8b393ed3 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
           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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
             } 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 8fa04e5338..20a80a1a35 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
     
       /* 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
       /** {@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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
                     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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
               } 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 f163e8d206..f42a1c6086 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 @@ import org.junit.runner.RunWith;
     @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/424] 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 35990573ad..24cf4062f7 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 final class MediaSessionConnector {
         }
     
         @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 final class MediaSessionConnector {
               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 4a8f8709e9..b5f8f954bb 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 @@ package com.google.android.exoplayer2;
     
     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 final class ExoPlaybackException extends Exception {
        * @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 final class ExoPlaybackException extends Exception {
        * @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/424] 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 a860d96e43..f372a47df6 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 @@ public class DownloadTracker {
         }
     
         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 9de6c748fb..ea79204c46 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 @@ public abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { */ 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 abstract class DownloadService extends Service { */ 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 abstract class DownloadService extends Service { */ 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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * 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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 57e7b8de5f..5a9ce2d88e 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 @@ public class DownloadServiceDashTest { 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/424] 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 ad45f61d98..76972a3530 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 186fdb1621..6c6ddf4ce4 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/424] 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 015b348f68..342ca55cc9 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 f8326dd503..723546726a 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/424] 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 9fc9834e1d..35db882e2a 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.DownloadManager; 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 class DownloadManagerDashTest { } // Disabled due to flakiness. - @Ignore @Test public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { handleDownloadRequest(fakeStreamKey1); @@ -204,7 +204,6 @@ public class DownloadManagerDashTest { } // Disabled due to flakiness [Internal: b/122290449]. - @Ignore @Test public void testHandleInterferingRemoveAction() throws Throwable { final ConditionVariable downloadInProgressCondition = new ConditionVariable(); @@ -260,6 +259,7 @@ public class DownloadManagerDashTest { 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/424] 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 a7ee6eb86e..37774bccb5 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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/424] 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 2c3e4f1e7c..1eb0977847 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 final class Log { 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 final class Log { 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 final class Log { 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 final class Log { 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/424] 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 2c9cd43d1e..6985d42b36 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 @@ public class DemoApplication extends Application { 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 @@ public class DemoApplication extends Application { } } - 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 b601874f8d..975fc10b93 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 @@ public final class 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 final class ActionFileUpgradeUtil { 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 final class ActionFileUpgradeUtil { if (downloadIdProvider != null) { request = request.copyWithId(downloadIdProvider.getId(request)); } - mergeRequest(request, downloadIndex); + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted); } success = true; } finally { @@ -85,10 +87,14 @@ public final class ActionFileUpgradeUtil { * * @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 final class ActionFileUpgradeUtil { 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 96b8ff21bc..dba7b74e9f 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 class ActionFileUpgradeUtilTest { 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 class ActionFileUpgradeUtilTest { /* customCacheKey= */ "key123", data); - ActionFileUpgradeUtil.mergeRequest(request, downloadIndex); + ActionFileUpgradeUtil.mergeRequest( + request, downloadIndex, /* addNewDownloadAsCompleted= */ false); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -135,8 +140,10 @@ public class ActionFileUpgradeUtilTest { 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 class ActionFileUpgradeUtilTest { 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/424] 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 d4df5cd18b..74332c08f3 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 final class DownloadManager { } /** - * 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 ea79204c46..ee00cf3d5f 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 @@ public abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { * @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 abstract class DownloadService extends Service { .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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 abstract class DownloadService extends Service { 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 28aa37ee2a..babc4e49fb 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.Network; 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.Documented; 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 final class Requirements { 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/424] 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 364a5d03c5..4b9bfa8fa2 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 7061ab9fe7..6d00e1ce97 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/424] 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 723546726a..4761a1fbe0 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/424] 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 c9b0451f41..8a15c82c89 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 final class DownloadHelper { /** * 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 final class DownloadHelper { } 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/424] 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 74332c08f3..bfcb5174cc 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 android.os.Message; 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 @@ public final class DownloadManager { 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/424] 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 9f6b473208..00d81b392c 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 bfcb5174cc..0ca13e2385 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 @@ public final class DownloadManager { // 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 @@ public final class DownloadManager { 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 final class 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 final class DownloadManager { 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 final class DownloadManager { 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 final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { } 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/424] 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 0ca13e2385..91a767cfab 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 final class 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 final class DownloadManager { * * @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 final class DownloadManager { * * @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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } + 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } boolean isRemove = downloadInternal.isInRemoveState(); if (!isRemove) { - if (parallelDownloads == maxParallelDownloads) { + if (parallelDownloads == maxParallelDownloadsInternal) { return START_THREAD_TOO_MANY_DOWNLOADS; } parallelDownloads++; @@ -813,7 +846,12 @@ public final class DownloadManager { 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/424] 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 | 767 +++++++++--------- 1 file changed, 388 insertions(+), 379 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 91a767cfab..aa0cd12231 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 @@ public final class DownloadManager { 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 final class 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 final class DownloadManager { * 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,324 +562,6 @@ public final class DownloadManager { 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(); - } - mainHandler - .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, downloadThreads.size()) - .sendToTarget(); - return true; - } - - 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 void setDownloadsPausedInternal(boolean downloadsPaused) { - if (this.downloadsPausedInternal == downloadsPaused) { - return; - } - this.downloadsPausedInternal = downloadsPaused; - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } - } - - private void setNotMetRequirementsInternal( - @Requirements.RequirementFlags int notMetRequirements) { - if (this.notMetRequirements == notMetRequirements) { - return; - } - this.notMetRequirements = notMetRequirements; - logdFlags("Not met requirements are changed", notMetRequirements); - for (int i = 0; i < downloadInternals.size(); i++) { - downloadInternals.get(i).updateStopState(); - } - } - - 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); - return; - } - } else { - 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); - } - } - - 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) { - 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); - } else { - download = mergeRequest(download, request, stopReason); - logd("Download state is loaded for " + request.id); - } - 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)); - } 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 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); - } - 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; - } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; - } - boolean isRemove = downloadInternal.isInRemoveState(); - if (!isRemove) { - if (parallelDownloads == maxParallelDownloadsInternal) { - 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, - 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; - } - 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; - } - } - return null; - } - - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); - } - 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; @@ -955,9 +618,355 @@ public final class DownloadManager { } } + 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<>(); + } + + @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 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; + } + this.notMetRequirements = notMetRequirements; + logdFlags("Not met requirements are changed", notMetRequirements); + for (int i = 0; i < downloadInternals.size(); i++) { + downloadInternals.get(i).updateStopState(); + } + } + + 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; + } + } else { + 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); + } + } + + private void setMaxParallelDownloads(int maxParallelDownloads) { + this.maxParallelDownloads = maxParallelDownloads; + // TODO: Start or stop downloads if necessary. + } + + private void setMinRetryCount(int minRetryCount) { + this.minRetryCount = 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); + } 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); + } + } + + 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 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 onDownloadThreadContentLengthChanged(DownloadThread downloadThread) { + String downloadId = downloadThread.request.id; + getDownload(downloadId).setContentLength(downloadThread.contentLength); + } + + private void release() { + for (DownloadThread downloadThread : downloadThreads.values()) { + downloadThread.cancel(/* released= */ true); + } + downloadThreads.clear(); + downloadInternals.clear(); + thread.quit(); + synchronized (this) { + released = true; + 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); + } + 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; + } + 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; + } + + @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; + } + + private Download loadDownload(String id) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "loadDownload failed", e); + } + 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 !downloadsPaused && notMetRequirements == 0; + } + } + private static final class DownloadInternal { - private final DownloadManager downloadManager; + private final InternalHandler internalHandler; private Download download; @@ -967,8 +976,8 @@ public final class DownloadManager { 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 final class DownloadManager { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - downloadManager.startDownloadThread(this); + internalHandler.startDownloadThread(this); } } @@ -1034,7 +1043,7 @@ public final class DownloadManager { return; } this.contentLength = contentLength; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } private void updateStopState() { @@ -1045,12 +1054,12 @@ public final class DownloadManager { } } 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 @@ public final class DownloadManager { // 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 @@ public final class DownloadManager { private void setState(@Download.State int newState) { if (state != newState) { state = newState; - downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); } } @@ -1097,9 +1106,9 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 final class DownloadManager { // 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 final class DownloadManager { } 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 final class DownloadManager { 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/424] 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 342ca55cc9..0beec1ef81 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/424] 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 0beec1ef81..bb612ea319 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 bc36fc4f3b..58109c1666 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 androidx.annotation.Nullable; 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 final class FfmpegLibrary { 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 c12bae0a07..7ee88d8f0f 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 final class LibraryLoader { } 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/424] 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 83cb5b723c..972f651a41 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 class DefaultLoadControl implements LoadControl { /** 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 class DefaultLoadControl implements LoadControl { 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 class DefaultLoadControl implements LoadControl { */ 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 class DefaultLoadControl implements LoadControl { } return new DefaultLoadControl( allocator, - minBufferMs, + minBufferAudioMs, + minBufferVideoMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -200,7 +217,8 @@ public class DefaultLoadControl implements LoadControl { 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 class DefaultLoadControl implements LoadControl { 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 class DefaultLoadControl implements LoadControl { /** @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 class DefaultLoadControl implements LoadControl { boolean prioritizeTimeOverSizeThresholds) { this( allocator, - minBufferMs, + /* minBufferAudioMs= */ minBufferMs, + /* minBufferVideoMs= */ minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -256,7 +278,8 @@ public class DefaultLoadControl implements LoadControl { protected DefaultLoadControl( DefaultAllocator allocator, - int minBufferMs, + int minBufferAudioMs, + int minBufferVideoMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, @@ -267,17 +290,27 @@ public class DefaultLoadControl implements LoadControl { 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 class DefaultLoadControl implements LoadControl { @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 class DefaultLoadControl implements LoadControl { @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 @@ public class DefaultLoadControl implements LoadControl { } } + 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/424] Downloading documentation PiperOrigin-RevId: 245443109 --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bb612ea319..80650974e7 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/424] 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 aa0cd12231..2caf89155a 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 final class 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/424] Fix line break --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80650974e7..aac46647cc 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/424] 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 2caf89155a..aa0cd12231 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 final class 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/424] 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 24cf4062f7..9c80fabc50 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 class MediaSessionConnector { 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 final class MediaSessionConnector { @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/424] 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 aa0cd12231..7ad22e000a 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 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_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 @@ public final class DownloadManager { 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 final class DownloadManager { * download requirements are not met). */ public boolean isIdle() { - return activeDownloadCount == 0 && pendingMessages == 0; + return activeTaskCount == 0 && pendingMessages == 0; } /** @@ -465,7 +465,7 @@ public final class DownloadManager { mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. pendingMessages = 0; - activeDownloadCount = 0; + activeTaskCount = 0; initialized = false; downloads.clear(); } @@ -503,8 +503,8 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { 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 final class DownloadManager { this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; downloadInternals = new ArrayList<>(); - downloadThreads = new HashMap<>(); + activeTasks = new HashMap<>(); } @Override @@ -694,14 +694,14 @@ public final class DownloadManager { 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 final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } @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 @@ public final class DownloadManager { } 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 final class DownloadManager { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); } else if (isInRemoveState()) { - internalHandler.startDownloadThread(this); + internalHandler.startTask(this); } } @@ -1043,7 +1048,7 @@ public final class DownloadManager { return; } this.contentLength = contentLength; - internalHandler.onDownloadChangedInternal(this, getUpdatedDownload()); + internalHandler.onDownloadChanged(this, getUpdatedDownload()); } private void updateStopState() { @@ -1054,12 +1059,12 @@ public final class DownloadManager { } } 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 @@ public final class DownloadManager { // 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { private long contentLength; - private DownloadThread( + private Task( DownloadRequest request, Downloader downloader, DownloadProgress downloadProgress, @@ -1203,7 +1208,7 @@ public final class DownloadManager { } 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 f901b00f53..e07166a21c 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 @@ import java.util.List; @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 @@ import java.util.List; 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 5798e9df8c..92c6debdd8 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 class DownloadManagerTest { } @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 class DownloadManagerTest { 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/424] 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 7ad22e000a..8502a56ea7 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 @@ public final class DownloadManager { private boolean downloadsPaused; private int maxParallelDownloads; private int minRetryCount; + private int notMetRequirements; private RequirementsWatcher requirementsWatcher; /** @@ -212,7 +213,7 @@ public final class 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 final class DownloadManager { 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 final class DownloadManager { * @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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } 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 2306363cf5..00b08dc76a 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 @@ package com.google.android.exoplayer2.offline; 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}). + * 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. * - *

    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 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/424] 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 aac46647cc..9e69bcc917 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 92756f19cf..261c9b531c 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 @@ import java.util.Map; 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 @@ import java.util.Map; 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/424] 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 | 836 +++++++++--------- .../offline/WritableDownloadIndex.java | 7 + .../offline/ActionFileUpgradeUtilTest.java | 14 +- .../offline/DownloadManagerTest.java | 54 +- 6 files changed, 473 insertions(+), 471 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 975fc10b93..baf47772ab 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 final class ActionFileUpgradeUtil { 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 final class ActionFileUpgradeUtil { /* 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 252c058b88..06f308d1e9 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 final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 final class DefaultDownloadIndex implements WritableDownloadIndex { 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 8502a56ea7..b528d91759 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.Handler; 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.Assertions; 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 @@ public final class DownloadManager { // 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { private int maxParallelDownloads; private int minRetryCount; private int notMetRequirements; + private List downloads; private RequirementsWatcher requirementsWatcher; /** @@ -205,11 +184,13 @@ public final class 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 final class DownloadManager { * 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 final class DownloadManager { * #getDownloadIndex()} instead. */ public List getCurrentDownloads() { - return Collections.unmodifiableList(new ArrayList<>(downloads)); + return downloads; } /** Returns whether downloads are currently paused. */ @@ -475,10 +462,10 @@ public final class DownloadManager { } mainHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. + downloads = Collections.emptyList(); pendingMessages = 0; activeTaskCount = 0; initialized = false; - downloads.clear(); } } @@ -508,13 +495,9 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } - 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 @@ public final class DownloadManager { } 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 final class DownloadManager { this.maxParallelDownloads = maxParallelDownloads; this.minRetryCount = minRetryCount; this.downloadsPaused = downloadsPaused; - downloadInternals = new ArrayList<>(); + downloads = new ArrayList<>(); activeTasks = new HashMap<>(); } @@ -732,70 +665,91 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } 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); - } + Download download = getDownload(id, /* loadFromIndex= */ true); + if (download == null) { + Log.e(TAG, "Failed to remove nonexistent download: " + id); + return; } - } - - 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(); - } - } - } - - 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 @@ public final class DownloadManager { } } - private void onDownloadChanged(DownloadInternal downloadInternal, Download download) { - logd("Download state is changed", 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++; + } + } + } + + 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); + } + } + + 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; + } + + if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) { + return null; + } + + // We can start a download task. + download = putDownloadWithState(download, STATE_DOWNLOADING); + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ false, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + activeDownloadTaskCount++; + activeTask.start(); + return activeTask; + } + + 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); + } + } + + 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; + } + + // 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(); + } + + // Task event processing. + + 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 void onTaskStopped(Task task) { + String downloadId = task.request.id; + activeTasks.remove(downloadId); + + boolean isRemove = task.isRemove; + if (!isRemove) { + activeDownloadTaskCount--; + } + + if (task.isCanceled) { + syncTasks(); + return; + } + + Throwable finalError = task.finalError; + if (finalError != null) { + Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); + } + + 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(); + } + + syncTasks(); + } + + private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) { + download = + new Download( + download.request, + finalError == null ? STATE_COMPLETED : STATE_FAILED, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + download.stopReason, + finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN, + download.progress); + // 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); + 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(); + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).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); - } - 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; + 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"); } - return START_THREAD_WAIT_REMOVAL_TO_FINISH; + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); } - 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; - 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 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; - } + // Helper methods. - @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; - } - - private Download loadDownload(String id) { - try { - return downloadIndex.getDownload(id); - } catch (IOException e) { - Log.e(TAG, "loadDownload failed", e); - } - 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() { + private boolean canDownloadsRun() { return !downloadsPaused && notMetRequirements == 0; } - } - private static final class DownloadInternal { - - private final InternalHandler internalHandler; - - private Download download; - - // 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(InternalHandler internalHandler, Download download) { - this.internalHandler = internalHandler; - this.download = download; - state = download.state; - contentLength = download.contentLength; - stopReason = download.stopReason; - failureReason = download.failureReason; - } - - private void initialize() { - initialize(download.state); - } - - public void addRequest(DownloadRequest newRequest, int stopReason) { - download = mergeRequest(download, newRequest, stopReason); - initialize(); - } - - public void remove() { - initialize(STATE_REMOVING); - } - - public Download getUpdatedDownload() { - download = + 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(), - contentLength, - stopReason, - state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - download.progress); + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress)); + } + + 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 { + boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs; + downloads.set(changedIndex, download); + if (needsSort) { + Collections.sort(downloads, InternalHandler::compareStartTimes); + } + } + 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; } - public boolean isIdle() { - return state != STATE_DOWNLOADING && state != STATE_REMOVING && state != STATE_RESTARTING; - } - - @Override - public String toString() { - return download.request.id + ' ' + Download.getStateString(state); - } - - public void start() { - if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { - startOrQueue(); - } else if (isInRemoveState()) { - internalHandler.startTask(this); + @Nullable + private Download getDownload(String id, boolean loadFromIndex) { + int index = getDownloadIndex(id); + if (index != C.INDEX_UNSET) { + return downloads.get(index); } - } - - public void setStopReason(int stopReason) { - this.stopReason = stopReason; - updateStopState(); - } - - public boolean isInRemoveState() { - return state == STATE_REMOVING || state == STATE_RESTARTING; - } - - public void setContentLength(long contentLength) { - if (this.contentLength == contentLength) { - return; - } - this.contentLength = contentLength; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } - - private void updateStopState() { - Download oldDownload = download; - if (canStart()) { - if (state == STATE_STOPPED) { - startOrQueue(); - } - } else { - if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - internalHandler.stopDownloadTask(download.request.id); - setState(STATE_STOPPED); + if (loadFromIndex) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "Failed to load download: " + id, e); } } - if (oldDownload == download) { - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } + return null; } - 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()); - } - } - - 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); - } - } - - private void setState(@Download.State int newState) { - if (state != newState) { - state = newState; - internalHandler.onDownloadChanged(this, getUpdatedDownload()); - } - } - - 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 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 static int compareStartTimes(Download first, Download second) { + return Util.compareLong(first.startTimeMs, second.startTimeMs); } } @@ -1177,16 +1130,17 @@ public final class DownloadManager { // 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 final class DownloadManager { 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 @@ public final class DownloadManager { 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 00b08dc76a..ae634f8544 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 dba7b74e9f..2f36b7f48c 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 @@ import org.junit.runner.RunWith; @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 class ActionFileUpgradeUtilTest { data); ActionFileUpgradeUtil.mergeRequest( - request, downloadIndex, /* addNewDownloadAsCompleted= */ false); + request, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS); assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED); } @@ -141,9 +143,9 @@ public class ActionFileUpgradeUtilTest { /* 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 class ActionFileUpgradeUtilTest { /* 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 92c6debdd8..2b9ef11235 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 class DownloadManagerTest { task.assertCompleted(); runner.assertCreatedDownloaderCount(1); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -143,6 +146,7 @@ public class DownloadManagerTest { task.assertRemoved(); runner.assertCreatedDownloaderCount(2); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -158,6 +162,7 @@ public class DownloadManagerTest { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertFailed(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -174,6 +179,7 @@ public class DownloadManagerTest { downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); runner.getTask().assertCompleted(); downloadManagerListener.blockUntilTasksComplete(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); } @Test @@ -341,7 +347,7 @@ public class DownloadManagerTest { } @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 class DownloadManagerTest { 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 class DownloadManagerTest { } @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 class DownloadManagerTest { } @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 class DownloadManagerTest { } @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 class DownloadManagerTest { } @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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 class DownloadManagerTest { 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 @@ public class DownloadManagerTest { 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/424] 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 b528d91759..3e0375718b 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } + // 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 ee00cf3d5f..ce9087c6c8 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 @@ public abstract class DownloadService extends Service { // 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 abstract class DownloadService extends Service { @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/424] 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 790f5ca4e5..d79dead0d7 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 @@ import com.google.android.exoplayer2.util.Util; */ 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 00d81b392c..97dff8394e 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 ce9087c6c8..fdd7163a2c 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 abstract class DownloadService extends Service { @Override public void onCreate() { - logd("onCreate"); if (channelId != null) { NotificationUtil.createNotificationChannel( this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); @@ -541,7 +539,6 @@ public abstract class DownloadService extends Service { if (intentAction == null) { intentAction = ACTION_INIT; } - logd("onStartCommand action: " + intentAction + " startId: " + startId); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: @@ -573,7 +570,7 @@ public abstract class DownloadService extends Service { if (!intent.hasExtra(KEY_STOP_REASON)) { Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { - int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); downloadManager.setStopReason(contentId, stopReason); } break; @@ -598,13 +595,11 @@ public abstract class DownloadService extends Service { @Override public void onTaskRemoved(Intent rootIntent) { - logd("onTaskRemoved rootIntent: " + rootIntent); taskRemoved = true; } @Override public void onDestroy() { - logd("onDestroy"); isDestroyed = true; DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); boolean unschedule = !downloadManager.isWaitingForRequirements(); @@ -713,16 +708,8 @@ public abstract class DownloadService extends Service { } if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. stopSelf(); - logd("stopSelf()"); } else { - boolean stopSelfResult = stopSelfResult(lastStartId); - logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); - } - } - - private void logd(String message) { - if (DEBUG) { - Log.d(TAG, message); + stopSelfResult(lastStartId); } } 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 fc8e8b61a5..8572c9c7ca 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 @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util; @TargetApi(21) public final class PlatformScheduler implements Scheduler { + private static final boolean DEBUG = false; private static final String TAG = "PlatformScheduler"; 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/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index babc4e49fb..30cf452572 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 @@ -27,7 +27,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -56,8 +55,6 @@ public final class Requirements implements Parcelable { /** Requirement that the device is charging. */ public static final int DEVICE_CHARGING = 1 << 3; - private static final String TAG = "Requirements"; - @RequirementFlags private final int requirements; /** @param requirements A combination of requirement flags. */ @@ -135,7 +132,6 @@ public final class Requirements implements Parcelable { if (networkInfo == null || !networkInfo.isConnected() || !isInternetConnectivityValidated(connectivityManager)) { - logd("No network info, connection or connectivity."); return requirements & (NETWORK | NETWORK_UNMETERED); } @@ -172,7 +168,6 @@ public final class Requirements implements Parcelable { } Network activeNetwork = connectivityManager.getActiveNetwork(); if (activeNetwork == null) { - logd("No active network."); return false; } NetworkCapabilities networkCapabilities = @@ -180,16 +175,9 @@ public final class Requirements implements Parcelable { boolean validated = networkCapabilities == null || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - logd("Network capability validated: " + validated); return !validated; } - private static void logd(String message) { - if (Scheduler.DEBUG) { - Log.d(TAG, message); - } - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index d2ad357ff6..f0d0f37cdf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -28,7 +28,6 @@ import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import androidx.annotation.RequiresApi; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; /** @@ -53,8 +52,6 @@ public final class RequirementsWatcher { @Requirements.RequirementFlags int notMetRequirements); } - private static final String TAG = "RequirementsWatcher"; - private final Context context; private final Listener listener; private final Requirements requirements; @@ -75,7 +72,6 @@ public final class RequirementsWatcher { this.listener = listener; this.requirements = requirements; handler = new Handler(Util.getLooper()); - logd(this + " created"); } /** @@ -110,7 +106,6 @@ public final class RequirementsWatcher { } receiver = new DeviceStatusChangeReceiver(); context.registerReceiver(receiver, filter, null, handler); - logd(this + " started"); return notMetRequirements; } @@ -121,7 +116,6 @@ public final class RequirementsWatcher { if (networkCallback != null) { unregisterNetworkCallback(); } - logd(this + " stopped"); } /** Returns watched {@link Requirements}. */ @@ -129,14 +123,6 @@ public final class RequirementsWatcher { return requirements; } - @Override - public String toString() { - if (!Scheduler.DEBUG) { - return super.toString(); - } - return "RequirementsWatcher{" + requirements + '}'; - } - @TargetApi(23) private void registerNetworkCallbackV23() { ConnectivityManager connectivityManager = @@ -163,22 +149,14 @@ public final class RequirementsWatcher { int notMetRequirements = requirements.getNotMetRequirements(context); if (this.notMetRequirements != notMetRequirements) { this.notMetRequirements = notMetRequirements; - logd("notMetRequirements has changed: " + notMetRequirements); listener.onRequirementsStateChanged(this, notMetRequirements); } } - private static void logd(String message) { - if (Scheduler.DEBUG) { - Log.d(TAG, message); - } - } - private class DeviceStatusChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { - logd(RequirementsWatcher.this + " received " + intent.getAction()); checkRequirements(); } } @@ -200,7 +178,6 @@ public final class RequirementsWatcher { handler.post( () -> { 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 1b225d9a4d..b5a6f40424 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 @@ import android.content.Intent; /** 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 9d6223b8b1..4c334992b5 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 android.os.ConditionVariable; 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 final class TestDownloadManagerListener implements DownloadManager.Listen } private void assertStateInternal(String taskId, int expectedState, int timeoutMs) { - ArrayList receivedStates = new ArrayList<>(); while (true) { Integer state = null; try { @@ -150,25 +147,8 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen 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/424] 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 3e0375718b..e8b7eaf9b2 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.Handler; 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 final class 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 final class 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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { } } + @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 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { private volatile InternalHandler internalHandler; private volatile boolean isCanceled; - private Throwable finalError; + @Nullable private Throwable finalError; private long contentLength; @@ -1145,6 +1153,7 @@ public final class DownloadManager { 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 @@ public final class DownloadManager { 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 c05486bedf..97bcb68708 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 final class Util { * * @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 final class Util { * * @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/424] 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 c2acf3990b..bcb3ef4ad1 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/424] 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 ca20c769dc..d8126d4736 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 @@ public class DefaultDownloaderFactory implements DownloaderFactory { .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/424] 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 e8b7eaf9b2..3bf03dd3e8 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 final class DownloadManager { /** * 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 final class DownloadManager { * @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/424] 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 ed77f41c83..55a7a901ac 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 final class OfflineLicenseHelper { * @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 final class OfflineLicenseHelper { * @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 915e855d23..7fc46dc363 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 final class GlUtil { } /** - * 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 final class GlUtil { } /** - * 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 fdd59101e7..3d7e57bbd2 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 final class CanvasRenderer { * 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 final class CanvasRenderer { 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/424] 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 20a80a1a35..bc5443f365 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; idToKey.remove(removedIds.keyAt(i)); } removedIds.clear(); + newIds.clear(); } /** @@ -250,11 +254,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void onRemove(CachedContent cachedContent) { + public void onRemove(CachedContent cachedContent, boolean neverStored) { changed = true; } @@ -856,8 +871,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @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/424] 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 9e0fc245fc..8a983c543a 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 8c73c01d74..1568074f9f 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 da606cd166..8ba92054ff 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 11a9124f54..04a2aeb140 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 aef5a12a96..b4158736a8 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 f7b8272bcc..13ceaaf51f 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 af58d417d6..1cdd325278 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/424] 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 a7ce7eb9a0..7d13993655 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 @@ public final class UrlTemplate { 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/424] 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 f372a47df6..a913a9b891 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ @@ -86,11 +83,9 @@ public class DownloadTracker { } @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { + public DownloadRequest getDownloadRequest(Uri uri) { Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED - ? download.request.streamKeys - : Collections.emptyList(); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; } public void toggleDownload( diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index acb24adebe..35307eb5d8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -75,7 +76,6 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -457,33 +457,26 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } @ContentType int type = Util.inferContentType(uri, overrideExtension); - List offlineStreamKeys = getOfflineStreamKeys(uri); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) - .createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: { + default: throw new IllegalStateException("Unsupported type: " + type); - } } } - private List getOfflineStreamKeys(Uri uri) { - return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); - } - private DefaultDrmSessionManager buildDrmSessionManagerV18( UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 07ba438182..8c11810506 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -46,18 +46,21 @@ # Constructors accessed via reflection in DownloadHelper -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory --keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { +-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); + ** setStreamKeys(java.util.List); com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 8a15c82c89..755f7e0343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import androidx.annotation.Nullable; -import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -32,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; @@ -106,30 +107,13 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - @Nullable private static final Constructor DASH_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor HLS_FACTORY_CONSTRUCTOR; - @Nullable private static final Constructor SS_FACTORY_CONSTRUCTOR; - @Nullable private static final Method DASH_FACTORY_CREATE_METHOD; - @Nullable private static final Method HLS_FACTORY_CREATE_METHOD; - @Nullable private static final Method SS_FACTORY_CREATE_METHOD; - - static { - Pair<@NullableType Constructor, @NullableType Method> dashFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first; - DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> hlsFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first; - HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second; - Pair<@NullableType Constructor, @NullableType Method> ssFactoryMethods = - getMediaSourceFactoryMethods( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; - SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; - } + private static final MediaSourceFactory DASH_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + private static final MediaSourceFactory SS_FACTORY = + getMediaSourceFactory( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + private static final MediaSourceFactory HLS_FACTORY = + getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); /** * Creates a {@link DownloadHelper} for progressive streams. @@ -202,8 +186,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD), + DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -252,8 +235,7 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - createMediaSource( - uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD), + HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -302,11 +284,42 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), + SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** + * Utility method to create a MediaSource which only contains the tracks defined in {@code + * downloadRequest}. + * + * @param downloadRequest A {@link DownloadRequest}. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + MediaSourceFactory factory; + switch (downloadRequest.type) { + case DownloadRequest.TYPE_DASH: + factory = DASH_FACTORY; + break; + case DownloadRequest.TYPE_SS: + factory = SS_FACTORY; + break; + case DownloadRequest.TYPE_HLS: + factory = HLS_FACTORY; + break; + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(downloadRequest.uri); + default: + throw new IllegalStateException("Unsupported type: " + downloadRequest.type); + } + return factory.createMediaSource( + downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + } + private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; @@ -739,35 +752,54 @@ public final class DownloadHelper { } } - private static Pair<@NullableType Constructor, @NullableType Method> - getMediaSourceFactoryMethods(String className) { + private static MediaSourceFactory getMediaSourceFactory(String className) { Constructor constructor = null; + Method setStreamKeysMethod = null; Method createMethod = null; try { // LINT.IfChange Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(DataSource.Factory.class); + constructor = factoryClazz.getConstructor(Factory.class); + setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - } catch (Exception e) { + } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. + } catch (NoSuchMethodException | SecurityException e) { + // Something is wrong with the library or the proguard configuration. + throw new IllegalStateException(e); } - return Pair.create(constructor, createMethod); + return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static MediaSource createMediaSource( - Uri uri, - DataSource.Factory dataSourceFactory, - @Nullable Constructor factoryConstructor, - @Nullable Method createMediaSourceMethod) { - if (factoryConstructor == null || createMediaSourceMethod == null) { - throw new IllegalStateException("Module missing to create media source."); + private static final class MediaSourceFactory { + @Nullable private final Constructor constructor; + @Nullable private final Method setStreamKeysMethod; + @Nullable private final Method createMethod; + + public MediaSourceFactory( + @Nullable Constructor constructor, + @Nullable Method setStreamKeysMethod, + @Nullable Method createMethod) { + this.constructor = constructor; + this.setStreamKeysMethod = setStreamKeysMethod; + this.createMethod = createMethod; } - try { - Object factory = factoryConstructor.newInstance(dataSourceFactory); - return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + + private MediaSource createMediaSource( + Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { + if (constructor == null || setStreamKeysMethod == null || createMethod == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + Object factory = constructor.newInstance(dataSourceFactory); + if (streamKeys != null) { + setStreamKeysMethod.invoke(factory, streamKeys); + } + return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } } } From 0698bd1dbb5c4a8bf7f8253e5321dd56078226be Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 May 2019 18:05:26 +0100 Subject: [PATCH 061/424] 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 9e69bcc917..310b947fdd 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 06f308d1e9..ef4bd00f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -233,6 +233,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @Override + public void setStatesToRemoving() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_REMOVING); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 3bf03dd3e8..ec5ff81d97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -133,10 +133,11 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_TASK_STOPPED = 8; - private static final int MSG_CONTENT_LENGTH_CHANGED = 9; - private static final int MSG_UPDATE_PROGRESS = 10; - private static final int MSG_RELEASE = 11; + private static final int MSG_REMOVE_ALL_DOWNLOADS = 8; + private static final int MSG_TASK_STOPPED = 9; + private static final int MSG_CONTENT_LENGTH_CHANGED = 10; + private static final int MSG_UPDATE_PROGRESS = 11; + private static final int MSG_RELEASE = 12; private static final String TAG = "DownloadManager"; @@ -446,6 +447,12 @@ public final class DownloadManager { internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); } + /** Cancels all pending downloads and removes all downloaded data. */ + public void removeAllDownloads() { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget(); + } + /** * Stops the downloads and releases resources. Waits until the downloads are persisted to the * download index. The manager must not be accessed after this method has been called. @@ -652,6 +659,9 @@ public final class DownloadManager { id = (String) message.obj; removeDownload(id); break; + case MSG_REMOVE_ALL_DOWNLOADS: + removeAllDownloads(); + break; case MSG_TASK_STOPPED: Task task = (Task) message.obj; onTaskStopped(task); @@ -797,6 +807,36 @@ public final class DownloadManager { syncTasks(); } + private void removeAllDownloads() { + List terminalDownloads = new ArrayList<>(); + try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) { + while (cursor.moveToNext()) { + terminalDownloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load downloads."); + } + for (int i = 0; i < downloads.size(); i++) { + downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING)); + } + for (int i = 0; i < terminalDownloads.size(); i++) { + downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING)); + } + Collections.sort(downloads, InternalHandler::compareStartTimes); + try { + downloadIndex.setStatesToRemoving(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + ArrayList updateList = new ArrayList<>(downloads); + for (int i = 0; i < downloads.size(); i++) { + DownloadUpdate update = + new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + syncTasks(); + } + private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); @@ -1057,16 +1097,7 @@ public final class DownloadManager { // to set STATE_STOPPED either, because it doesn't have a stopReason argument. Assertions.checkState( state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); - return putDownload( - new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - /* stopReason= */ 0, - FAILURE_REASON_NONE, - download.progress)); + return putDownload(copyDownloadWithState(download, state)); } private Download putDownload(Download download) { @@ -1120,6 +1151,18 @@ public final class DownloadManager { return C.INDEX_UNSET; } + private static Download copyDownloadWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress); + } + private static int compareStartTimes(Download first, Download second) { return Util.compareLong(first.startTimeMs, second.startTimeMs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index fdd7163a2c..3900dc8e93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -77,6 +77,16 @@ public abstract class DownloadService extends Service { public static final String ACTION_REMOVE_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** + * Removes all downloads. Extras: + * + *

      + *
    • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
    + */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -296,6 +306,19 @@ public abstract class DownloadService extends Service { .putExtra(KEY_CONTENT_ID, id); } + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + /** * Builds an {@link Intent} for resuming all downloads. * @@ -414,6 +437,19 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + /** * Starts the service if not started already and resumes all downloads. * @@ -560,6 +596,9 @@ public abstract class DownloadService extends Service { downloadManager.removeDownload(contentId); } break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index ae634f8544..dc7085c85e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void setDownloadingStatesToQueued() throws IOException; + /** + * Sets all states to {@link Download#STATE_REMOVING}. + * + * @throws IOException If an error occurs updating the state. + */ + void setStatesToRemoving() throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 2b9ef11235..de430d1416 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -243,6 +243,27 @@ public class DownloadManagerTest { downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test + public void removeAllDownloads_removesAllDownloads() throws Throwable { + // Finish one download and keep one running. + DownloadRunner runner1 = new DownloadRunner(uri1); + DownloadRunner runner2 = new DownloadRunner(uri2); + runner1.postDownloadRequest(); + runner1.getDownloader(0).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + runner2.postDownloadRequest(); + + runner1.postRemoveAllRequest(); + runner1.getDownloader(1).unblock(); + runner2.getDownloader(1).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + runner1.getTask().assertRemoved(); + runner2.getTask().assertRemoved(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0); + } + @Test public void differentDownloadRequestsMerged() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1); @@ -605,6 +626,11 @@ public class DownloadManagerTest { return this; } + private DownloadRunner postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + return this; + } + private DownloadRunner postDownloadRequest(StreamKey... keys) { DownloadRequest downloadRequest = new DownloadRequest( From 85a86e434a6aa4be083afe38130818865622d061 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 04:40:24 +0100 Subject: [PATCH 062/424] 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 0185a6d8af..6fb0ac6856 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -60,7 +60,7 @@ import java.util.List; * The threshold number of samples to trim from the start/end of an audio track when applying an * edit below which gapless info can be used (rather than removing samples from the sample table). */ - private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); From ee5981c02dc1e6c465a463c2f8d826963619149b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 May 2019 14:40:51 +0100 Subject: [PATCH 063/424] 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 37774bccb5..03c3482eac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1053,11 +1053,14 @@ import java.util.concurrent.atomic.AtomicBoolean; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { - pendingMessages.remove(nextPendingMessageIndex); - } else { - nextPendingMessageIndex++; + try { + sendMessageToTarget(nextInfo.message); + } finally { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { + pendingMessages.remove(nextPendingMessageIndex); + } else { + nextPendingMessageIndex++; + } } nextInfo = nextPendingMessageIndex < pendingMessages.size() From 29add854af1b9ad2a645a229da8c601807731d52 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 15:10:41 +0100 Subject: [PATCH 064/424] 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 910404a875..697f35e417 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1231,7 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/faqs.html#" + + "https://exoplayer.dev/troubleshooting.html#" + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; From ac07c56dab4b5d90f17731d3b5e878a9b154206a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 16:23:02 +0100 Subject: [PATCH 065/424] 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 310b947fdd..4f05b0a78d 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 ef233bb566..2cfd14c79d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - roleFlags = mediaTagFormat.roleFlags; + roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; } From 6ead14880bea2471add1fcea1f8fa026d06d7a61 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 17:13:36 +0100 Subject: [PATCH 066/424] 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 4f05b0a78d..4ee6c64444 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 07769e7d85..e75f7ffc7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -786,7 +786,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); - if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) { mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } @@ -809,6 +809,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the device's decoders are known to not support setting the codec operating + * rate. + * + *

    See GitHub issue #5821. + */ + private static boolean deviceDoesntSupportOperatingRate() { + return Util.SDK_INT == 23 + && ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL)); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. From 1b9d018296ae5c2a6fa6bf23ed9b563d12e804c5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 18:12:05 +0100 Subject: [PATCH 067/424] 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 7f74216cc8..3400cf25b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -59,7 +59,7 @@ public interface AnalyticsListener { public final Timeline timeline; /** - * Window index in the {@code timeline} this event belongs to, or the prospective window index + * Window index in the {@link #timeline} this event belongs to, or the prospective window index * if the timeline is not yet known and empty. */ public final int windowIndex; @@ -76,7 +76,7 @@ public interface AnalyticsListener { public final long eventPlaybackPositionMs; /** - * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the + * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the * currently playing ad at the time of the event, in milliseconds. */ public final long currentPlaybackPositionMs; @@ -91,15 +91,15 @@ public interface AnalyticsListener { * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at * the time of the event, in milliseconds. * @param timeline Timeline at the time of the event. - * @param windowIndex Window index in the {@code timeline} this event belongs to, or the + * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the * prospective window index if the timeline is not yet known and empty. * @param mediaPeriodId Media period identifier for the media period this event belongs to, or * {@code null} if the event is not associated with a specific media period. * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time * of the event, in milliseconds. - * @param currentPlaybackPositionMs Position in the current timeline window ({@code - * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, - * in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@link + * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in + * milliseconds. * @param totalBufferedDurationMs Total buffered duration from {@link * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes * pre-buffered data for subsequent ads and windows. From bef386bea8c9fe4ef3e765a46503a49a0401d1ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 11:56:35 +0100 Subject: [PATCH 068/424] 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 4b9bfa8fa2..31ff0ad6b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.enableUnitTestBinaryResources=true buildDir=buildout +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m From 48de1010a8ca84dcc89c0f6c139d644719acc6e0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 May 2019 16:05:34 +0100 Subject: [PATCH 069/424] 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 4ee6c64444..6a49f911d7 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 d04cd3a999..489719eda4 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 9cbcea5814..97aac9995d 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 final class IcyDecoderTest { assertThat(streamInfo.url).isEqualTo("test_url"); } + @Test + public void decode_lineTerminatorInTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_notIcy() { IcyDecoder decoder = new IcyDecoder(); From 035686e58cd45916aac05e15e6c24f30350a77b6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 16:21:33 +0100 Subject: [PATCH 070/424] 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 a818ea390e..74fcc3dd6c 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -18,10 +18,13 @@ android.libraryVariants.all { variant -> if (!name.equals("release")) { return; // Skip non-release builds. } + def allSourceDirs = variant.sourceSets.inject ([]) { + acc, val -> acc << val.javaDirectories + } task("generateJavadoc", type: Javadoc) { description = "Generates Javadoc for the ${javadocTitle}." title = "ExoPlayer ${javadocTitle}" - source = variant.javaCompileProvider.get().source + source = allSourceDirs options { links "http://docs.oracle.com/javase/7/docs/api/" linksOffline "https://developer.android.com/reference", From cea3071b333618161d09931ee4dcab3d14fa3125 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 May 2019 12:33:19 +0100 Subject: [PATCH 071/424] 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 6a49f911d7..55349ad42e 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 eb956f06db..3f2fef454f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Region; import android.util.SparseArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; @@ -150,6 +149,8 @@ import java.util.List; List cues = new ArrayList<>(); SparseArray pageRegions = subtitleService.pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); PageRegion pageRegion = pageRegions.valueAt(i); int regionId = pageRegions.keyAt(i); RegionComposition regionComposition = subtitleService.regions.get(regionId); @@ -163,9 +164,7 @@ import java.util.List; displayDefinition.horizontalPositionMaximum); int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, displayDefinition.verticalPositionMaximum); - canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, - Region.Op.REPLACE); - + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); if (clutDefinition == null) { clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); @@ -214,9 +213,11 @@ import java.util.List; (float) regionComposition.height / displayDefinition.height)); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); } - return cues; + return Collections.unmodifiableList(cues); } // Static parsing. From 3ce0d89c56fa8d0a53b3ed82bd4dc67e58ef877a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 May 2019 13:42:16 +0100 Subject: [PATCH 072/424] 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 55349ad42e..fa2baceac3 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 489719eda4..3d873926bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';", 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 97aac9995d..4602d172a6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -48,6 +48,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isNull(); } + @Test + public void decode_emptyTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEmpty(); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); From 6e9df31e7d432d1c1c5196a35cfcd26d12bd8bef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 14 May 2019 23:18:42 +0100 Subject: [PATCH 073/424] 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 7831033b99..1076716684 100644 --- a/library/dash/README.md +++ b/library/dash/README.md @@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/dash.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md index 1dd1b7a62e..3470c29e3c 100644 --- a/library/hls/README.md +++ b/library/hls/README.md @@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/hls.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md index 4fa24543d6..d53471d17c 100644 --- a/library/smoothstreaming/README.md +++ b/library/smoothstreaming/README.md @@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/smoothstreaming.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md index 341ea2fb16..16136b3d94 100644 --- a/library/ui/README.md +++ b/library/ui/README.md @@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/ui-components.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html From 7f89fa9a8ce63d56f840352c79e7360e445d5402 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 May 2019 17:50:50 +0100 Subject: [PATCH 074/424] 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 a9995af0e4..ca196b1d2f 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final CronetEngine cronetEngine; private final Executor executor; - private final Predicate contentTypePredicate; + @Nullable private final Predicate contentTypePredicate; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. + */ + public CronetDataSource(CronetEngine cronetEngine, Executor executor) { + this(cronetEngine, executor, /* contentTypePredicate= */ null); + } + /** * @param cronetEngine A CronetEngine. * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may @@ -158,7 +170,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * #open(DataSpec)}. */ public CronetDataSource( - CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) { + CronetEngine cronetEngine, + Executor executor, + @Nullable Predicate contentTypePredicate) { this( cronetEngine, executor, @@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index a749495184..8eb8bba920 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private long bytesSkipped; private long bytesRead; + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + * @param userAgent An optional User-Agent string. + */ + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { + this(callFactory, userAgent, /* contentTypePredicate= */ null); + } + /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 6aad517004..66036b7a84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private long bytesSkipped; private long bytesRead; + /** @param userAgent The User-Agent string that should be used. */ + public DefaultHttpDataSource(String userAgent) { + this(userAgent, /* contentTypePredicate= */ null); + } + /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java index 6669504c07..fd7be241df 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java @@ -27,7 +27,7 @@ import java.io.IOException; /** Fake {@link MediaChunk}. */ public final class FakeMediaChunk extends MediaChunk { - private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT"); public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs); From 8efaf5fd7d5bdf1f55f35109a43380e8f7f6be0b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 075/424] 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 9c80fabc50..06f1cee001 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 @@ public final class MediaSessionConnector { 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 final class MediaSessionConnector { } 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 final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1190,7 +1189,7 @@ public final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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/424] 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 06f1cee001..9c80fabc50 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 @@ public final class MediaSessionConnector { 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 final class MediaSessionConnector { } 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 final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - setPlayWhenReady(/* playWhenReady= */ false); + stopPlayerForPrepare(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1189,7 +1190,7 @@ public final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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/424] 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 465ad51ac5..f1316b2bfb 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -948,8 +948,8 @@ public final class ImaAdsLoader @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { - // The player is being reset and this source will be released. + if (timeline.isEmpty()) { + // The player is being reset or contains no media. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); From 15b319cba24fcca91c18de6a111b0994651bbee1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 May 2019 12:30:13 +0100 Subject: [PATCH 078/424] 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 fa2baceac3..9e7a992e11 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 5063c59141..b2ee322ee6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.0' - releaseVersionCode = 2010000 + releaseVersion = '2.10.1' + releaseVersionCode = 2010001 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 72760db31b..a90435227b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.10.0"; + public static final String VERSION = "2.10.1"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2010000; + public static final int VERSION_INT = 2010001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 9ec330e7c771d33b8cb7ac043eb52aee4af4b316 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 May 2019 12:38:07 +0100 Subject: [PATCH 079/424] 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 8572c9c7ca..e6679e1a5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.util.Util; * * * - * * } From 6fa58f8d695657f901f59c9e4d2807c7949a33ce Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 22 May 2019 19:45:48 +0100 Subject: [PATCH 080/424] 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 690069ffa8..a4996278bd 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/424] 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 f326e7cd46..ff29f3a7d1 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/424] 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 089de35910..d481de33ce 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/424] 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 3ed569862f..a68e4e70e1 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/424] 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 9c80fabc50..06f1cee001 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 @@ public final class MediaSessionConnector { 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 final class MediaSessionConnector { } 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 final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1190,7 +1189,7 @@ public final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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 final class MediaSessionConnector { @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/424] 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 9e7a992e11..7d7085af24 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 06f1cee001..c0b5fd67f6 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 @@ public final class MediaSessionConnector { 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 final class MediaSessionConnector { * @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 @@ public final class MediaSessionConnector { 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 final class MediaSessionConnector { 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 final class MediaSessionConnector { @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/424] 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 7d7085af24..527f906405 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 a5deb808c1..0b83615807 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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { @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 class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { 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/424] 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 f4a7976ebd..27e6a5b3b8 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/424] 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 93461c1b24..a38d61b1b1 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.util.AttributeSet; 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.ExoPlaybackException; 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 class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider break; case SURFACE_TYPE_MONO360_VIEW: SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); - sphericalSurfaceView.setSurfaceListener(componentListener); sphericalSurfaceView.setSingleTapListener(componentListener); surfaceView = sphericalSurfaceView; break; @@ -1359,7 +1356,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener, SingleTapListener { // TextOutput implementation @@ -1449,18 +1445,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider 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 1029a28323..02b3043665 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 @@ import javax.microedition.khronos.opengles.GL10; */ 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 final class SphericalSurfaceView extends GLSurfaceView { 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 final class SphericalSurfaceView extends GLSurfaceView { } } - /** - * 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 @@ public final class SphericalSurfaceView extends GLSurfaceView { 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 @@ public final class SphericalSurfaceView extends GLSurfaceView { 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/424] 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 527f906405..333fe5c314 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 328b5d6a49..5c70203788 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 class DefaultTimeBar extends View implements TimeBar { 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 0b83615807..383d796692 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.KeyEvent; 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 @@ import java.util.Locale; *

  • 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 @@ import java.util.Locale; *
      *
    • 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 class PlayerControlView extends FrameLayout { 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 a38d61b1b1..8e94d96739 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 @@ import java.util.List; *
  • 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 @@ import java.util.List; *
  • 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 class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider 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 ed2fb8e2b2..027e57ee92 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 27e6a5b3b8..706fba0e0b 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 e57301f946..17b55cd731 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/424] 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 a91bbbd981..2df9448d08 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/424] 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 0de29eea32..2c5b64f8bd 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/424] 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 333fe5c314..6a46ffd5dc 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 755f7e0343..7e98f30301 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 @@ public final class DownloadHelper { 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/424] 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 f7855810d4..be08186dc0 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.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -742,11 +741,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { 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/424] 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 65039b9364..434b6c2011 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 @@ import java.util.Map; 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/424] 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 4f22362de6..9ed1bbd006 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 @@ import com.google.android.exoplayer2.util.Util; 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/424] 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 2c5b64f8bd..be75eae359 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 59bf5f8f87..ed28f07acb 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 eab6862555..18f1dd5c69 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/424] 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 6a46ffd5dc..219d0fc23c 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 2a977f5bba..490d961396 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 androidx.annotation.Nullable; 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 class DefaultRenderersFactory implements RenderersFactory { 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 class DefaultRenderersFactory implements RenderersFactory { mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, videoRendererEventListener, allowedVideoJoiningTimeMs, @@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory { mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, buildAudioProcessors(), eventHandler, audioRendererEventListener, @@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory { * @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 @@ public class DefaultRenderersFactory implements RenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, @@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory { allowedVideoJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -356,6 +378,9 @@ public class DefaultRenderersFactory implements RenderersFactory { * @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 @@ public class DefaultRenderersFactory implements RenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory { 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 e75f7ffc7b..a86eb97a37 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 class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @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 70059114db..92ec23c34d 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 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { 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/424] 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 22330e0a05..30d7b8e92c 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 final class VpxOutputBuffer extends OutputBuffer { * 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 final class VpxOutputBuffer extends OutputBuffer { 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 82c023afbc..9fc8b09a18 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/424] 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 04a90b38d8..0120451bc1 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 @@ public final class 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 @@ public final class 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 @@ public final class 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 ffcd893e7b..bd57c82916 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 @@ public final class DefaultAudioSink implements AudioSink { 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 @@ public final class DefaultAudioSink implements AudioSink { 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 a86eb97a37..d43bd6cbf8 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 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @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 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @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 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * 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 e603f76dbc..61457c308d 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 final class MimeTypes { 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/424] 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 219d0fc23c..8ea7feff29 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 b39f467968..6e0c495466 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 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /* 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 ecf5c8b0a0..3b4d061aaa 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 @@ import java.util.TreeSet; 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 2b1e9cf99a..3cbc25d4b2 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 @@ import com.google.android.exoplayer2.text.Cue; 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 @@ import com.google.android.exoplayer2.text.Cue; /* 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 @@ import com.google.android.exoplayer2.text.Cue; @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width, + float height, int textSizeType, float textSize) { this.id = id; @@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue; 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 000d0634ce..85af6482c0 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 final class TtmlDecoderTest { 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 final class TtmlDecoderTest { 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 final class TtmlDecoderTest { 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 final class TtmlDecoderTest { 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 final class TtmlDecoderTest { 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/424] 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 8ea7feff29..acb22ab35d 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 8e94d96739..f92d550706 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 class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider 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/424] 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 4dc463ff81..e067789bc4 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/424] 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 73e8ac4f3e..39429a8fa1 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 final class HostActivity extends Activity implements SurfaceHolder.Callba 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/424] 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 acb22ab35d..474570088f 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 bbf57c5602..0adadd87c2 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 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { 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 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { 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/424] 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 474570088f..90c3874cd7 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 f92d550706..c7ffda8ae5 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 class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } @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/424] 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 90c3874cd7..49fa49ba77 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 58b2d176cf..e5df8d55c3 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 final class CacheDataSource implements DataSource { 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 final class CacheDataSource implements DataSource { } 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 @@ public final class CacheDataSource implements DataSource { 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 219d736835..9c80becdeb 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 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.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 final class CacheUtil { 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 final class CacheUtil { * 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 @@ public final class CacheUtil { PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -263,22 +271,23 @@ public final class CacheUtil { // 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 final class CacheUtil { } } + /*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 664532d3ff..e095c55939 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 final class CacheAsserts { * @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 final class CacheAsserts { 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/424] 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 9c80becdeb..5b066b7930 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 final class CacheUtil { 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 final class CacheUtil { 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 final class CacheUtil { } } + 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/424] 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 | 102 +++++++++++------- 4 files changed, 90 insertions(+), 55 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 b3a6b8271b..94dae35ed5 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.Downloader; 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 class DashDownloaderTest { 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 class DashDownloaderTest { 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 class DashDownloaderTest { 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 class DashDownloaderTest { 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 class DashDownloaderTest { // 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 35db882e2a..280bc45b70 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.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.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 class DownloadManagerDashTest { 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 class DownloadManagerDashTest { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -176,7 +177,7 @@ public class DownloadManagerDashTest { 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 7d77a78316..d06d047f66 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.DownloaderConstructorHelper; 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 class HlsDownloaderTest { 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 class HlsDownloaderTest { 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 e095c55939..00c9e60bd5 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 @@ import java.util.ArrayList; /** 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); + } + } + + 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, String... uriStrings) - throws IOException { - Uri[] uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - 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, 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/424] 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 49fa49ba77..80aa49f3f4 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 5b066b7930..47470c5de7 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 @@ public final class CacheUtil { 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 @@ public final class CacheUtil { } 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/424] 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 80aa49f3f4..3ab6c7bd7a 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 9316e4fb86..774b94a43c 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 final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); + isInCaptionService = true; } @Override @@ -288,6 +305,7 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = 0; repeatableControlCc2 = 0; currentChannel = NTSC_CC_CHANNEL_1; + isInCaptionService = true; } @Override @@ -363,6 +381,12 @@ public final class Cea608Decoder extends CeaDecoder { 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 @@ public final class Cea608Decoder extends CeaDecoder { 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 @@ public final class Cea608Decoder extends CeaDecoder { 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/424] 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 3ab6c7bd7a..17df5def0c 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 0000000000..99f0dee207 --- /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/424] 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 17df5def0c..6780ea97e6 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 0000000000..b03dd0ea7c --- /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/424] 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 6780ea97e6..e7be123c8b 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 b2ee322ee6..bf464ad2c1 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 a90435227b..db3f3943e1 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/424] 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 14bb433d2b..0cf31c1a46 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; 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 final class CastPlayer extends BasePlayer { 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 final class CastPlayer extends BasePlayer { @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 final class CastPlayer extends BasePlayer { 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 final class CastPlayer extends BasePlayer { || 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 @@ public final class CastPlayer extends BasePlayer { @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 final class CastPlayer extends BasePlayer { } - // 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 final class CastPlayer extends BasePlayer { 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/424] 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 e7be123c8b..22c61066d1 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 0cf31c1a46..4b973715b1 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 final class CastPlayer extends BasePlayer { 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 final class CastPlayer extends BasePlayer { new ListenerNotificationTask( listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } - maybeUpdateTimelineAndNotify(); flushNotifications(); } @@ -714,16 +723,6 @@ public final class CastPlayer extends BasePlayer { } } - /** - * 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/424] 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 4b71b3a001..df153a1423 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 @@ import java.util.ArrayList; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -268,9 +267,6 @@ import java.util.ArrayList; 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 @@ import java.util.ArrayList; this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -352,12 +347,11 @@ import java.util.ArrayList; */ 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/424] 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 be08186dc0..5f7f5d60b7 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 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @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 193fbddfec..e75a3866b6 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( @@ -1173,11 +1172,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { 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 92ec23c34d..9feaf6863a 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.DrmSessionManager; 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 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { 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/424] 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 a4996278bd..c0980df440 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/424] 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 ff29f3a7d1..c8d4668a6a 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 0000000000..01801c9897 --- /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/424] 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 06f0bec971..4ce45a92b1 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.CastMediaControlIntent; 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 final class DefaultCastOptionsProvider implements OptionsProvider { @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 2d74bce5f1..9b15aff846 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 @@ import java.util.List; } @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 de038921aa..a97d99fa54 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 @@ import java.nio.ByteBuffer; } } - 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 @@ import java.nio.ByteBuffer; /** * 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 @@ import java.nio.ByteBuffer; /** * 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 @@ import java.nio.ByteBuffer; /** * 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 @@ import java.nio.ByteBuffer; 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 @@ import java.nio.ByteBuffer; 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 56acbdb7d3..0795079c6b 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 f8ec477b88..dbce33b923 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 @@ import java.util.List; } @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 285be96388..2c2c8f6972 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 final class OpusLibrary { 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 57e5481b55..0e13e82630 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 @@ import java.nio.ByteBuffer; } @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 5a65fc56ff..db056d5110 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 final class VpxLibrary { 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 final class VpxLibrary { * 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 f8204f6be3..b5650860e9 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 @@ public abstract class SimpleDecoder< * @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/424] 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 | 36 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 192 ++++++++---------- 5 files changed, 119 insertions(+), 123 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 435279fc45..934d7cf106 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 final class FlacBinarySearchSeekerTest { 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 final class FlacBinarySearchSeekerTest { 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 d9cbac6ad5..97f152cea4 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 @@ import org.junit.runner.RunWith; 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 9b15aff846..d20c18e957 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 @@ package com.google.android.exoplayer2.ext.flac; 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 @@ import java.util.List; 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 a97d99fa54..32ef22dab0 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 @@ package com.google.android.exoplayer2.ext.flac; 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 @@ import java.nio.ByteBuffer; @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 @@ import java.nio.ByteBuffer; } /** - * 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; - if (tempBuffer == null) { - this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; - } endOfExtractorInput = false; + if (tempBuffer == null) { + tempBuffer = new byte[TEMP_BUFFER_SIZE]; + } } + /** + * 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 @@ import java.nio.ByteBuffer; *

    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 @@ import java.nio.ByteBuffer; } /** 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 bb72e114fe..491b962129 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.IntDef; 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 final class FlacExtractor implements Extractor { */ 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 final class FlacExtractor implements Extractor { @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); - - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.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; - } + decodeStreamInfo(input); - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return handlePendingSeek(input, seekPosition); + } + + 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; + } + + 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 final class FlacExtractor implements Extractor { } /** - * 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 @@ public final class FlacExtractor implements Extractor { */ 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); + 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 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(); + binarySearchSeeker = + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + } extractorOutput.seekMap(seekMap); } - private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET) { - long firstFramePosition = decoderJni.getDecodePosition(); - flacBinarySearchSeeker = - 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()); - } - } - - private void outputFormat(FlacStreamInfo streamInfo) { + private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -277,25 +271,15 @@ public final class FlacExtractor implements Extractor { /* 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/424] 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 491b962129..b50554e2f6 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.SeekPoint; 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 final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { /* 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/424] 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 06a5888404..10b244cb39 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 b50554e2f6..082068f34d 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.Retention; 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 final class FlacExtractor implements Extractor { * @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 final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { 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 @@ public final class FlacExtractor implements Extractor { } 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/424] 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 eb1cc8f336..806cc9fad4 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 @@ import java.util.Map; private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -138,7 +140,10 @@ import java.util.Map; 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 @@ import java.util.Map; 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 @@ import java.util.Map; 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 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,8 +220,8 @@ import java.util.Map; 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 9d3635e8b3..7676926c4d 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 final class Track { * @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 5bd29c6e75..a35d211aa4 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 4b54b3ea9a..3f6ff44248 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 @@ import java.lang.annotation.RetentionPolicy; */ 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 38d6ff25cb..bd561afaf8 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 @@ public abstract class SimpleSubtitleDecoder extends @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 75b7a01673..843cfab045 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 @@ package com.google.android.exoplayer2.text; 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 abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti @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 091bda49f3..9ef3556c8f 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 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { 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 c25b26128c..b1af75f613 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 final class SsaDecoder extends SimpleSubtitleDecoder { * 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 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { 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 339119ed6b..9a3756194f 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 @@ import java.util.List; 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 @@ import java.util.List; @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 6f9fd366ec..5dfaecee1d 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { 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 a79df478e5..01ed1711a9 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 @@ import java.util.List; 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 @@ import java.util.List; @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 9211dc51ce..ddc7a8f5f8 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 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } 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/424] 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 4b973715b1..bc0987322b 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 final class CastPlayer extends BasePlayer { * 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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { * @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 final class CastPlayer extends BasePlayer { /** * 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 final class CastPlayer extends BasePlayer { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -529,7 +536,7 @@ public final class CastPlayer extends BasePlayer { // 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 @@ public final class CastPlayer extends BasePlayer { } } - 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/424] 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 d4a37ea4ef..268219b6d5 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 @@ package com.google.android.exoplayer2.ui; 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 @@ public final class AspectRatioFrameLayout extends FrameLayout { 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 final class AspectRatioFrameLayout extends FrameLayout { /** * 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 383d796692..73bb98a1a0 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 class PlayerControlView extends FrameLayout { 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 5d99eda109..0bdc1acc88 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 final class SubtitleView extends View implements TextOutput { @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 final class SubtitleView extends View implements TextOutput { 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/424] 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 | 42 ++++++-- 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, 423 insertions(+), 87 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 33cab06819..03298229d6 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 ee0a9fa5b5..b44dab3445 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 934d7cf106..a3770afc78 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 final class FlacBinarySearchSeekerTest { 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 final class FlacBinarySearchSeekerTest { 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 b9c6ea06dd..4bfcc003ec 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.BinarySearchSeeker; 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 @@ import java.nio.ByteBuffer; 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 @@ import java.nio.ByteBuffer; * 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 d20c18e957..50eb048d98 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.ParserException; 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 @@ import java.util.List; } 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 @@ import java.util.List; } 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 32ef22dab0..f454e28c68 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 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.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -142,13 +142,13 @@ import java.nio.ByteBuffer; 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 @@ import java.nio.ByteBuffer; 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 082068f34d..151875c2c5 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.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.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 final class FlacExtractor implements Extractor { 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 final class FlacExtractor implements Extractor { @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 @@ public final class FlacExtractor implements Extractor { } @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 @@ public final class FlacExtractor implements Extractor { @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 298719d48d..4ba071e1ca 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"); + "FlacStreamMetadata"); + jmethodID flacStreamMetadataConstructor = env->GetMethodID( + flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;)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); + 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 83d3367415..b2d074252d 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 cea7fbe33b..d9043e9548 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 5eb0727908..d4c2bbb485 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.Format; 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 @@ import java.util.List; 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 @@ import java.util.List; protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ import java.util.List; 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 @@ import java.util.List; @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 0000000000..b1951cbc13 --- /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 0df39e103d..43fdda367e 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 final class FlacStreamInfo { 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 final class FlacStreamInfo { * @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 final class 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 final class 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 final class FlacStreamInfo { } 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 0000000000..868b28b0e1 --- /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 0392f8b26d..2a1c59e7df 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 androidx.test.ext.junit.runners.AndroidJUnit4; 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 0000000000..325d9b19f6 --- /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/424] 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 c3072a1590..a8cf0f12e2 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 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ 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/424] 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 03298229d6..05e0e45ca8 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 c004058082..a10416fac8 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 @@ import java.util.concurrent.CopyOnWriteArrayList; 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 a9fe73371a..65a6866a9f 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 bc1ea7b1e1..2733df7ba6 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 @@ import com.google.android.exoplayer2.util.Util; 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 86fa5e11ee..2927d03114 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 @@ import com.google.android.exoplayer2.util.Assertions; 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 @@ import com.google.android.exoplayer2.util.Assertions; } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +571,7 @@ import com.google.android.exoplayer2.util.Assertions; // 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 @@ import com.google.android.exoplayer2.util.Assertions; 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 @@ import com.google.android.exoplayer2.util.Assertions; 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 0792bf0c7d..7107963c83 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 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * 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 a715289a04..440a84bacb 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 static org.junit.Assert.fail; 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 final class ExoPlayerTest { 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 517f1ce2e7..2f91c1926c 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 final class ExoPlayerTestRunner implements Player.EventListener, ActionSc 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/424] 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 05e0e45ca8..17190cfc0d 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 de4a75d607..94a6e21c86 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 final class DataSchemeDataSource extends BaseDataSource { 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 final class DataSchemeDataSource extends BaseDataSource { // 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 final class DataSchemeDataSource extends BaseDataSource { 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 2df9a608e9..8cb142f05d 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 static org.junit.Assert.fail; 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 @@ import org.junit.runner.RunWith; @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 final class DataSchemeDataSourceTest { @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 final class DataSchemeDataSourceTest { 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 final class DataSchemeDataSourceTest { } 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/424] 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 17190cfc0d..f0813034e0 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 949bd178ea..b8dd40f8bd 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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 86ad6fd6b3..919cda76c1 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.Calendar; 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 final class Util { 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 @@ public final class Util { } } + 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 9abec0cd8f..f85ee37c07 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 class UtilTest { @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 class UtilTest { @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 095739271e..254a2b2bd1 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 class HlsMasterPlaylistParserTest { 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/424] 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 f0813034e0..7bc7e1129b 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 919cda76c1..095394b2f5 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 final class Util { 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 @@ public final class Util { } 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 f85ee37c07..5a13ed0dd8 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 class UtilTest { @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 class UtilTest { @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/424] 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 03a54947cf..85e60f2796 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 856b0b1235..dbfdd833f6 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/424] 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 8d5b890c7f..591a10087c 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { 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/424] 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 7bc7e1129b..5deb0c5168 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 3886ef5c44..c3909dfe46 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 class DemoDownloadService extends DownloadService { 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 3900dc8e93..6587984f0c 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 @@ public abstract class DownloadService extends Service { 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 @@ public abstract class DownloadService extends Service { * 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 abstract class DownloadService extends Service { public void onCreate() { if (channelId != null) { NotificationUtil.createNotificationChannel( - this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); } 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 4cd03f566d..756494f9d0 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 cedd3dbec5..260fb9d398 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 class PlayerNotificationManager { 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 class PlayerNotificationManager { * * @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 class PlayerNotificationManager { 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 class PlayerNotificationManager { * @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 class PlayerNotificationManager { 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/424] Update release notes --- RELEASENOTES.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5deb0c5168..de4d474e5c 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/424] 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 0bf6baa4d0..6cd46bb705 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 @@ import java.util.Arrays; 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 @@ import java.util.Arrays; 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 0d938d33f4..bd32e5ee6f 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 final class SonicAudioProcessor implements AudioProcessor { 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 final class SonicAudioProcessor implements AudioProcessor { @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/424] 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 70e77b22c6..9e532e053b 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 190f4de5a6..f420f20767 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/424] 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 f70125ba38..bd76868625 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 d79dead0d7..c8975275f1 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 @@ import com.google.android.exoplayer2.util.Util; * * @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/424] 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 bd76868625..5d59e64466 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/424] 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 5d59e64466..c822c14ce8 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/424] 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 c822c14ce8..8be027d308 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/424] 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 8be027d308..712b76fb28 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/424] 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 de4d474e5c..16818c867e 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 b03dd0ea7c..72095c2c54 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 final class SilenceMediaSource extends BaseMediaSource { @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 final class SilenceMediaSource extends BaseMediaSource { @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 final class SilenceMediaSource extends BaseMediaSource { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -172,6 +174,10 @@ public final class SilenceMediaSource extends BaseMediaSource { @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 final class SilenceMediaSource extends BaseMediaSource { } 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/424] 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 712b76fb28..a6f0c3966a 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/424] 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 d7a1c75302..0d5a168197 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 @@ public interface TrackOutput { * 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/424] 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 c65ad0bc67..e42a10a75f 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 final class Mp3Extractor implements Extractor { /* 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/424] 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 c83662ee83..9700760c49 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 @@ import java.io.IOException; * 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 @@ import java.io.IOException; * @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 @@ import java.io.IOException; * 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 bbf7e2fc6b..bb84909f67 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; * 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/424] 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 e42a10a75f..bc218e26ad 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 final class Mp3Extractor implements Extractor { /* 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/424] 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 16818c867e..7fea201237 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 b44dab3445..3e52f643e7 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 151875c2c5..cd91b06288 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 @@ public final class FlacExtractor implements Extractor { 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 4ba071e1ca..d60a7cead2 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 b2d074252d..830f3e2178 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 d9043e9548..14ba9e8725 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 0000000000..ce134614ad --- /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 b1951cbc13..9f44cdf393 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 43fdda367e..2c814294af 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 @@ 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 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 final class FlacStreamMetadata { 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 final class FlacStreamMetadata { * @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 final class 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 final class 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 final class FlacStreamMetadata { } @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 0000000000..3f07dbc26d --- /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 868b28b0e1..bb118e381a 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 325d9b19f6..72a80161f2 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 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 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 final class FlacStreamMetadataTest { 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 final class FlacStreamMetadataTest { 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 final class FlacStreamMetadataTest { 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 final class FlacStreamMetadataTest { 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 e6bc1a6a71..1e7d6407e6 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.PlaybackPreparer; 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 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } 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/424] 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 374c15eea0..4f59c19795 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 @@ public final class MediaCodecUtil { 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 @@ public final class MediaCodecUtil { || "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/424] Some no-op cleanup for DefaultOggSeeker PiperOrigin-RevId: 261102008 --- .../extractor/ogg/DefaultOggSeeker.java | 110 ++++++++---------- 1 file changed, 49 insertions(+), 61 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 9700760c49..a4aa6b8dd5 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.ExtractorInput; 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 @@ import java.io.IOException; return -(pageHeader.granulePosition + 2); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * 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); } - 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); - } - + input.resetPeekPosition(); + return currentGranule; } /** @@ -266,8 +260,7 @@ import java.io.IOException; * @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 @@ import java.io.IOException; 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); - } - input.resetPeekPosition(); - return currentGranule; - } + private final class OggSeekMap implements SeekMap { + @Override + public boolean isSeekable() { + return true; + } + + @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/424] 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 a4aa6b8dd5..308547e510 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 @@ import java.io.IOException; } @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 d4c2bbb485..4efd5c5e11 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 @@ import java.util.List; } @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 aa88e5bf89..e4c3a163e6 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 @@ import java.io.IOException; 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 e459ad1e58..35a07fcf49 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; 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/424] 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 7c5864420a..35b67e1068 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 @@ import java.util.List; 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 6fb0ac6856..70873825e3 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 @@ import java.util.List; 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 @@ import java.util.List; 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/424] 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 | 175 +++++++++--------- .../video/MediaCodecVideoRenderer.java | 6 +- 5 files changed, 122 insertions(+), 116 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7fea201237..39cc640807 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 ace7ebbcc6..5cf40a6741 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 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media 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 2158f182b1..7fc748485b 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 final class MediaCodecInfo { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,10 @@ public final class MediaCodecInfo { * * @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 final class MediaCodecInfo { 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 4f59c19795..a6391e4cc7 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 final class MediaCodecUtil { 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 final class MediaCodecUtil { * * @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 @@ public final class MediaCodecUtil { 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 @@ public final class MediaCodecUtil { || (!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 @@ public final class MediaCodecUtil { } 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 @@ public final class MediaCodecUtil { } /** - * 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; + } - 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"; + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; } } + + 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 @@ public final class MediaCodecUtil { * @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 @@ public final class MediaCodecUtil { // 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 @@ public final class MediaCodecUtil { 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 @@ public final class MediaCodecUtil { 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 @@ public final class MediaCodecUtil { 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 @@ public final class MediaCodecUtil { && ("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 @@ public final class MediaCodecUtil { && "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 @@ public final class MediaCodecUtil { } // 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 591a10087c..b5a935c15f 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * 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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @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/424] 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 5cf40a6741..07a1438519 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 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media 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/424] 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 c7b7a40ead..d76d3f37ea 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.extractor.ExtractorInput; 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 @@ import java.io.IOException; 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/424] 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 68d252e318..d3114f9b69 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 final class WavExtractor implements Extractor { 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 c60117be60..c7858dcd96 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 @@ import com.google.android.exoplayer2.util.Util; @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 @@ import com.google.android.exoplayer2.util.Util; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +58,33 @@ import com.google.android.exoplayer2.util.Util; /** * 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 d76d3f37ea..839a9e3d5c 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 @@ import java.io.IOException; // 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/424] 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 | 36 +++++++++++-------- .../extractor/wav/WavHeaderReader.java | 13 +++++-- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39cc640807..9bc77f8cfc 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 d3114f9b69..91097c9e5b 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 final class WavExtractor implements Extractor { 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 c7858dcd96..6e3c5988a9 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 @@ import com.google.android.exoplayer2.util.Util; 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; + /** 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) { + 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 @@ import com.google.android.exoplayer2.util.Util; this.bitsPerSample = bitsPerSample; this.encoding = encoding; dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -59,11 +64,11 @@ import com.google.android.exoplayer2.util.Util; * 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 @@ import com.google.android.exoplayer2.util.Util; } /** - * 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 @@ import com.google.android.exoplayer2.util.Util; @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 839a9e3d5c..bbcb75aa2d 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 @@ import java.io.IOException; // 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 @@ import java.io.IOException; // 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/424] 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 440a84bacb..2203b34e86 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 final class ExoPlayerTest { /* 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/424] 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 9bc77f8cfc..829f8b70df 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 87bb992082..e454bd51c8 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 final class MpegAudioHeader { } } - // 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 d4df3ffeba..96b0cd259c 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 d4df3ffeba..96b0cd259c 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 d4df3ffeba..96b0cd259c 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 d4df3ffeba..96b0cd259c 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/424] 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 | 177 ++++++++---------- .../extractor/ogg/StreamReader.java | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 107 +++++------ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 95 +--------- 4 files changed, 129 insertions(+), 252 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 308547e510..064bd5732d 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; /** * 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; 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; - } - - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); - - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + 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 @@ import java.io.IOException; */ @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 @@ import java.io.IOException; */ 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 @@ import java.io.IOException; 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 @@ import java.io.IOException; 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 35a07fcf49..d2671125e4 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 @@ import java.io.IOException; 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 8d1818845d..fba358ea51 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 @@ public final class DefaultOggSeekerTest { 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 @@ public final class DefaultOggSeekerTest { 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 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(); - 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 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 @@ public final class DefaultOggSeekerTest { 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 @@ public final class DefaultOggSeekerTest { } @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 d6691f50f8..2521602228 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 @@ public final class DefaultOggSeekerUtilMethodsTest { 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 @@ public final class DefaultOggSeekerUtilMethodsTest { } } - @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 final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (EOFException e) { - // ignored + // Ignored. } } @@ -216,7 +135,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (IllegalArgumentException e) { - // ignored + // Ignored. } } @@ -224,9 +143,9 @@ public final class DefaultOggSeekerUtilMethodsTest { 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 @@ public final class DefaultOggSeekerUtilMethodsTest { 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/424] 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 3937dabcaf..a933121bc5 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 @@ public final class ExtractorAsserts { 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 c467bd36af..1a127eeab5 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 @@ public final class FakeExtractorInput implements ExtractorInput { 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 c6543bd7a5..4022a0ccc1 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 final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab 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/424] 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 fba358ea51..fd649f0924 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 static org.junit.Assert.fail; 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 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { @@ -55,6 +61,95 @@ public final class DefaultOggSeekerTest { } } + @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 @@ public final class DefaultOggSeekerTest { } } - 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 2521602228..0000000000 --- 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/424] 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 064bd5732d..51ab94ba0e 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 @@ import java.io.IOException; /** 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 @@ import java.io.IOException; @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 @@ import java.io.IOException; 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 fd649f0924..8ba0be26a0 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 @@ public final class DefaultOggSeekerTest { /* 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 @@ public final class DefaultOggSeekerTest { 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 e5512dda36..38e4332b16 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 @@ import java.util.Random; 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 @@ import java.util.Random; 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 @@ import java.util.Random; 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 @@ import java.util.Random; } 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/424] Remove obsolete workaround PiperOrigin-RevId: 261340526 --- build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build.gradle b/build.gradle index bc538ead68..1d0b459bf5 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/424] 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 e067789bc4..83e994c5e1 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 76972a3530..34ad80b7ed 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 9065855a3f..ea7564316f 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/424] 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 33161b4121..124555d9b5 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 7089d4d731..06c5d1ffb7 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 83e994c5e1..68a7494a3f 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 34ad80b7ed..b2dd6bc889 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 ffecdcd16f..15952b1860 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 10b244cb39..c67de27697 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 50acd6c040..1031d6f4b7 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 2df9448d08..0ef9f281c9 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 c6f5a216ce..ecaa78e25b 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 db2e073c8a..68bd422185 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 0795079c6b..28f7b05465 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 ca734c3657..b74be659ee 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 02b68b831d..92450f0381 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 68ff8cc977..e633e12057 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 f6981a2220..9f5775d478 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 8e9696af70..82e09ab72c 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 a2e81fb304..fa67ea1d01 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 6384bf920f..5182dfccf5 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 dd5cfa64a7..5865d3c36d 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 bdc26d5c19..1ec358b83d 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 a3859a9e48..758d22b5d9 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/424] 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 829f8b70df..d9f534a4c8 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 1e7d6407e6..95d2e1c1bb 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 class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1492,9 +1495,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @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/424] 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 d9f534a4c8..a3f6c1ebfc 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 b40bbb35d1..c84847f755 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 @@ public interface MediaPeriod extends SequenceableLoader { * 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 40d4e468bd..f7edf62182 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 @@ public interface DashChunkSource extends ChunkSource { * @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 431a0a4bd9..8635005bfc 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 @@ import java.util.regex.Pattern; 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 057f0262d0..396d16968f 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 class DefaultDashChunkSource implements DashChunkSource { 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 class DefaultDashChunkSource implements DashChunkSource { protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @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 261c9b531c..ee5a5f0809 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 @@ import java.util.Map; } /** - * 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 434b6c2011..f7bc913527 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 @@ import java.util.Map; 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 59e18195e2..22dfb04f13 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 class DefaultSsChunkSource implements SsChunkSource { 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 class DefaultSsChunkSource implements SsChunkSource { 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 b763a484b8..111393140e 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 @@ public interface SsChunkSource extends ChunkSource { * @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 135ee4a58e..e325439d05 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 @@ import java.util.List; stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } From 97183ef55866170807910cd626264d82d41d46d4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:02 +0100 Subject: [PATCH 215/424] Add inband emsg-v1 support to FragmentedMp4Extractor This also decouples EventMessageEncoder's serialization schema from the emesg spec (it happens to still match the emsg-v0 spec, but this is no longer required). PiperOrigin-RevId: 261877918 --- .../java/com/google/android/exoplayer2/C.java | 7 +- .../extractor/mp4/FragmentedMp4Extractor.java | 92 ++++++++++++------- .../metadata/emsg/EventMessageDecoder.java | 8 +- 3 files changed, 66 insertions(+), 41 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 0120451bc1..cf0f97ea76 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 @@ -71,9 +71,10 @@ public final class C { /** Represents an unset or unknown percentage. */ public static final int PERCENTAGE_UNSET = -1; - /** - * The number of microseconds in one second. - */ + /** The number of milliseconds in one second. */ + public static final long MILLIS_PER_SECOND = 1000L; + + /** The number of microseconds in one second. */ public static final long MICROS_PER_SECOND = 1000000L; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 4f45e85762..373fd3f14e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -35,6 +35,8 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -140,6 +142,8 @@ public class FragmentedMp4Extractor implements Extractor { // Adjusts sample timestamps. private final @Nullable TimestampAdjuster timestampAdjuster; + private final EventMessageEncoder eventMessageEncoder; + // Parser state. private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; @@ -253,6 +257,7 @@ public class FragmentedMp4Extractor implements Extractor { this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -590,39 +595,71 @@ public class FragmentedMp4Extractor implements Extractor { } } - /** - * Parses an emsg atom (defined in 23009-1). - */ + /** Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { return; } + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - atom.readNullTerminatedString(); // schemeIdUri - atom.readNullTerminatedString(); // value - long timescale = atom.readUnsignedInt(); - long presentationTimeDeltaUs = - Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); - - // The presentation_time_delta is accounted for by adjusting the sample timestamp, so we zero it - // in the sample data before writing it to the track outputs. - int position = atom.getPosition(); - atom.data[position - 4] = 0; - atom.data[position - 3] = 0; - atom.data[position - 2] = 0; - atom.data[position - 1] = 0; + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); // Output the sample data. for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { - atom.setPosition(Atom.FULL_HEADER_SIZE); - emsgTrackOutput.sampleData(atom, sampleSize); + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); } - // Output the sample metadata. - if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - long sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else { if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -630,17 +667,10 @@ public class FragmentedMp4Extractor implements Extractor { emsgTrackOutput.sampleMetadata( sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); } - } else { - // We need the first sample timestamp in the segment before we can output the metadata. - pendingMetadataSampleInfos.addLast( - new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); - pendingMetadataSampleBytes += sampleSize; } } - /** - * Parses a trex atom (defined in 14496-12). - */ + /** Parses a trex atom (defined in 14496-12). */ private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 33d79917eb..87d0491a7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -25,13 +25,7 @@ import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.Arrays; -/** - * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3. - * - *

    Atom data should be provided to the decoder without the full atom header (i.e. starting from - * the first byte of the scheme_id_uri field). It is expected that the presentation_time_delta field - * should be 0, having already been accounted for by adjusting the sample timestamp. - */ +/** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { private static final String TAG = "EventMessageDecoder"; From 9f486336beda728b8e90324559b316e0eeebede9 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:40 +0100 Subject: [PATCH 216/424] Migrate literal usages of 1000 to (new) C.MILLIS_PER_SECOND This only covers calls to scaleLargeTimestamp() PiperOrigin-RevId: 261878019 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 9 ++++++--- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 4 +++- .../source/dash/manifest/DashManifestParser.java | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 373fd3f14e..3bf4604687 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -964,7 +964,9 @@ public class FragmentedMp4Extractor implements Extractor { // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; @@ -992,12 +994,13 @@ public class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset; + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 87d0491a7b..a49bf956b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -46,7 +47,8 @@ public final class EventMessageDecoder implements MetadataDecoder { // timestamp and zeroing the field in the sample data. Log a warning if the field is non-zero. Log.w(TAG, "Ignoring non-zero presentation_time_delta: " + presentationTimeDelta); } - long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); + long durationMs = + Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index f03a443431..c3dfc3f136 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -906,7 +906,7 @@ public class DashManifestParser extends DefaultHandler long id = parseLong(xpp, "id", 0); long duration = parseLong(xpp, "duration", C.TIME_UNSET); long presentationTime = parseLong(xpp, "presentationTime", 0); - long durationMs = Util.scaleLargeTimestamp(duration, 1000, timescale); + long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale); long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, timescale); String messageData = parseString(xpp, "messageData", null); From c4ac166f2f89173b756ec6c386a996e531bd5b49 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 17:28:15 +0100 Subject: [PATCH 217/424] Add allowAudioMixedChannelCountAdaptiveness parameter to DefaultTrackSelector. We already allow mixed mime type and mixed sample rate adaptation on request, so for completeness, we can also allow mixed channel count adaptation. Issue:#6257 PiperOrigin-RevId: 261930046 --- RELEASENOTES.md | 7 +++ .../trackselection/DefaultTrackSelector.java | 55 ++++++++++++++++--- .../DefaultTrackSelectorTest.java | 1 + 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a3f6c1ebfc..133f68195d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,12 @@ # Release notes # +### 2.10.5 ### + +* Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts + ([#6257](https://github.com/google/ExoPlayer/issues/6257)). + ### 2.10.4 ### * Offline: Add `Scheduler` implementation that uses `WorkManager`. 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 b8dd40f8bd..006c281ec7 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 @@ -178,6 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedAudioConstraintsIfNecessary; private boolean allowAudioMixedMimeTypeAdaptiveness; private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; @@ -215,6 +216,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; // General forceLowestBitrate = initialValues.forceLowestBitrate; forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; @@ -412,6 +415,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + // Text @Override @@ -628,6 +642,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary, allowAudioMixedMimeTypeAdaptiveness, allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, selectUndeterminedTextLanguage, @@ -749,6 +764,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * different sample rates may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedSampleRateAdaptiveness; + /** + * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations + * between different channel counts may not be completely seamless. The default value is {@code + * false}. + */ + public final boolean allowAudioMixedChannelCountAdaptiveness; // General /** @@ -809,6 +830,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, @@ -841,6 +863,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedAudioConstraintsIfNecessary, boolean allowAudioMixedMimeTypeAdaptiveness, boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -875,6 +898,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; // General this.forceLowestBitrate = forceLowestBitrate; this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; @@ -908,6 +932,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in); this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in); this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in); + this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in); // General this.forceLowestBitrate = Util.readBoolean(in); this.forceHighestSupportedBitrate = Util.readBoolean(in); @@ -989,6 +1014,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness + && allowAudioMixedChannelCountAdaptiveness + == other.allowAudioMixedChannelCountAdaptiveness // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate @@ -1019,6 +1046,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0); result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); // General result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); @@ -1055,6 +1083,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary); Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness); Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness); + Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness); // General Util.writeBoolean(dest, forceLowestBitrate); Util.writeBoolean(dest, forceHighestSupportedBitrate); @@ -1936,7 +1965,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { formatSupports[selectedGroupIndex], params.maxAudioBitrate, params.allowAudioMixedMimeTypeAdaptiveness, - params.allowAudioMixedSampleRateAdaptiveness); + params.allowAudioMixedSampleRateAdaptiveness, + params.allowAudioMixedChannelCountAdaptiveness); if (adaptiveTracks.length > 0) { definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } @@ -1954,7 +1984,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int selectedConfigurationTrackCount = 0; AudioConfigurationTuple selectedConfiguration = null; HashSet seenConfigurationTuples = new HashSet<>(); @@ -1971,7 +2002,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness); + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -1991,7 +2023,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectedConfiguration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -2006,7 +2039,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int count = 0; for (int i = 0; i < group.length; i++) { if (isSupportedAdaptiveAudioTrack( @@ -2015,7 +2049,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } @@ -2028,11 +2063,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { return isSupported(formatSupport, false) && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) - && (format.channelCount != Format.NO_VALUE - && format.channelCount == configuration.channelCount) + && (allowAudioMixedChannelCountAdaptiveness + || (format.channelCount != Format.NO_VALUE + && format.channelCount == configuration.channelCount)) && (allowMixedMimeTypeAdaptiveness || (format.sampleMimeType != null && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) 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 224b2965ba..9941ae1098 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 @@ -143,6 +143,7 @@ public final class DefaultTrackSelectorTest { /* exceedAudioConstraintsIfNecessary= */ false, /* allowAudioMixedMimeTypeAdaptiveness= */ true, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ true, // Text /* preferredTextLanguage= */ "de", /* selectUndeterminedTextLanguage= */ true, From acdb19e99d119110af4d062f1e3431198ca6fa41 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 11:36:41 +0100 Subject: [PATCH 218/424] Clean up documentation of DefaultTrackSelector.ParametersBuilder. We don't usually refer to other classes when documenting method parameters but rather duplicate the actual definition. PiperOrigin-RevId: 262102714 --- .../trackselection/DefaultTrackSelector.java | 131 +++++++++++++----- .../TrackSelectionParameters.java | 20 ++- 2 files changed, 109 insertions(+), 42 deletions(-) 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 006c281ec7..762e0a98b4 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 @@ -249,8 +249,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}. + * Sets the maximum allowed video width and height. * + * @param maxVideoWidth Maximum allowed video width in pixels. + * @param maxVideoHeight Maximum allowed video height in pixels. * @return This builder. */ public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { @@ -260,8 +262,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoFrameRate}. + * Sets the maximum allowed video frame rate. * + * @param maxVideoFrameRate Maximum allowed video frame rate in hertz. * @return This builder. */ public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { @@ -270,8 +273,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoBitrate}. + * Sets the maximum allowed video bitrate. * + * @param maxVideoBitrate Maximum allowed video bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { @@ -280,8 +284,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedVideoConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedVideoConstraintsIfNecessary( @@ -291,8 +298,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive video selections containing mixed MIME types. * + *

    Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. + * + * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( @@ -302,8 +315,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoNonSeamlessAdaptiveness}. + * Sets whether to allow adaptive video selections where adaptation may not be completely + * seamless. * + * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where + * adaptation may not be completely seamless. * @return This builder. */ public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( @@ -317,7 +333,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * * @param context Any context. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSizeToPhysicalDisplaySize( @@ -338,12 +355,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link - * Parameters#viewportOrientationMayChange}. + * Sets the viewport size to constrain adaptive video selections so that only tracks suitable + * for the viewport are selected. * - * @param viewportWidth See {@link Parameters#viewportWidth}. - * @param viewportHeight See {@link Parameters#viewportHeight}. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportWidth Viewport width in pixels. + * @param viewportHeight Viewport height in pixels. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSize( @@ -363,8 +381,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioChannelCount}. + * Sets the maximum allowed audio channel count. * + * @param maxAudioChannelCount Maximum allowed audio channel count. * @return This builder. */ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { @@ -373,8 +392,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioBitrate}. + * Sets the maximum allowed audio bitrate. * + * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { @@ -383,8 +403,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedAudioConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedAudioConstraintsIfNecessary( @@ -394,8 +417,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed MIME types. * + *

    Adaptations between different MIME types may not be completely seamless. + * + * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( @@ -405,8 +432,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedSampleRateAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed sample rates. * + *

    Adaptations between different sample rates may not be completely seamless. + * + * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections + * containing mixed sample rates. * @return This builder. */ public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( @@ -416,8 +447,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed channel counts. * + *

    Adaptations between different channel counts may not be completely seamless. + * + * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections + * containing mixed channel counts. * @return This builder. */ public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( @@ -450,8 +485,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General /** - * See {@link Parameters#forceLowestBitrate}. + * Sets whether to force selection of the single lowest bitrate audio and video tracks that + * comply with all other constraints. * + * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and + * video tracks. * @return This builder. */ public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { @@ -460,8 +498,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#forceHighestSupportedBitrate}. + * Sets whether to force selection of the highest bitrate audio and video tracks that comply + * with all other constraints. * + * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio + * and video tracks. * @return This builder. */ public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { @@ -487,8 +528,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}. + * Sets whether to exceed renderer capabilities when no selection can be made otherwise. * + *

    This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( @@ -498,7 +546,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#tunnelingAudioSessionId}. + * Sets the audio session id to use when tunneling. * *

    Enables or disables tunneling. To enable tunneling, pass an audio session id to use when * in tunneling mode. Session ids can be generated using {@link @@ -508,6 +556,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + * @return This builder. */ public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { this.tunnelingAudioSessionId = tunnelingAudioSessionId; @@ -522,6 +571,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param disabled Whether the renderer is disabled. + * @return This builder. */ public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { if (rendererDisabledFlags.get(rendererIndex) == disabled) { @@ -558,6 +608,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be applied. * @param override The override. + * @return This builder. */ public final ParametersBuilder setSelectionOverride( int rendererIndex, TrackGroupArray groups, SelectionOverride override) { @@ -579,6 +630,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be cleared. + * @return This builder. */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { @@ -598,6 +650,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Clears all track selection overrides for the specified renderer. * * @param rendererIndex The renderer index. + * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { Map overrides = selectionOverrides.get(rendererIndex); @@ -609,7 +662,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } - /** Clears all track selection overrides for all renderers. */ + /** + * Clears all track selection overrides for all renderers. + * + * @return This builder. + */ public final ParametersBuilder clearSelectionOverrides() { if (selectionOverrides.size() == 0) { // Nothing to clear. @@ -677,8 +734,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Video /** - * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

    To constrain adaptive video track selections to be suitable for a given viewport (the * region of the display within which video will be played), use ({@link #viewportWidth}, {@link @@ -686,8 +743,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoWidth; /** - * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

    To constrain adaptive video track selections to be suitable for a given viewport (the * region of the display within which video will be played), use ({@link #viewportWidth}, {@link @@ -695,12 +752,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoHeight; /** - * Maximum allowed video frame rate. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int maxVideoFrameRate; /** - * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed video bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** @@ -710,9 +768,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Whether to allow adaptive video selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless, in which case {@link - * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed mime type + * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless, in which case {@link + * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type * selections to be made. The default value is {@code false}. */ public final boolean allowVideoMixedMimeTypeAdaptiveness; @@ -746,7 +804,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxAudioChannelCount; /** - * Maximum audio bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed audio bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxAudioBitrate; /** @@ -755,8 +814,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedAudioConstraintsIfNecessary; /** - * Whether to allow adaptive audio selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless. The default value is {@code false}. + * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedMimeTypeAdaptiveness; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 66a4707496..f10b2befaf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -57,9 +57,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#preferredAudioLanguage}. + * Sets the preferred language for audio and forced text tracks. * - * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag. + * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track, or the first track if there's no default. * @return This builder. */ public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { @@ -70,9 +71,10 @@ public class TrackSelectionParameters implements Parcelable { // Text /** - * See {@link TrackSelectionParameters#preferredTextLanguage}. + * Sets the preferred language for text tracks. * - * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag. + * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track if there is one, or no track otherwise. * @return This builder. */ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { @@ -81,8 +83,12 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#selectUndeterminedTextLanguage}. + * Sets whether a text track with undetermined language should be selected if no track with + * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is + * unset. * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should + * be selected if no preferred language track is available. * @return This builder. */ public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { @@ -91,8 +97,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#disabledTextTrackSelectionFlags}. + * Sets a bitmask of selection flags that are disabled for text track selections. * + * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are + * disabled for text track selections. * @return This builder. */ public Builder setDisabledTextTrackSelectionFlags( From bb6b0e1a5aff65742e779817fc41a08757894361 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 14:18:22 +0100 Subject: [PATCH 219/424] Expose a method on EventMessageDecoder that returns EventMessage directly PiperOrigin-RevId: 262121134 --- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index a49bf956b3..340b662e97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -37,7 +37,10 @@ public final class EventMessageDecoder implements MetadataDecoder { ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); - ParsableByteArray emsgData = new ParsableByteArray(data, size); + return new Metadata(decode(new ParsableByteArray(data, size))); + } + + public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); long timescale = emsgData.readUnsignedInt(); @@ -50,8 +53,9 @@ public final class EventMessageDecoder implements MetadataDecoder { long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); long id = emsgData.readUnsignedInt(); - byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } } From a08b537e8eefcc04747c5810344d3080988d2a1a Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 15:56:25 +0100 Subject: [PATCH 220/424] Simplify EventMessageEncoder/Decoder serialization We're no longer tied to the emsg spec, so we can skip unused fields and assume ms for duration. Also remove @Nullable annotation from EventMessageEncoder#encode, it seems the current implementation never returns null PiperOrigin-RevId: 262135009 --- .../metadata/emsg/EventMessageDecoder.java | 15 +--- .../metadata/emsg/EventMessageEncoder.java | 4 -- .../emsg/EventMessageDecoderTest.java | 19 ++--- .../emsg/EventMessageEncoderTest.java | 69 ++++++++----------- 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 340b662e97..f592a6eee7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,22 +15,17 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; 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.nio.ByteBuffer; import java.util.Arrays; /** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { - private static final String TAG = "EventMessageDecoder"; - @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { @@ -43,15 +38,7 @@ public final class EventMessageDecoder implements MetadataDecoder { public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long timescale = emsgData.readUnsignedInt(); - long presentationTimeDelta = emsgData.readUnsignedInt(); - if (presentationTimeDelta != 0) { - // We expect the source to have accounted for presentation_time_delta by adjusting the sample - // timestamp and zeroing the field in the sample data. Log a warning if the field is non-zero. - Log.w(TAG, "Ignoring non-zero presentation_time_delta: " + presentationTimeDelta); - } - long durationMs = - Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + long durationMs = emsgData.readUnsignedInt(); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index dd33d591a7..4fa3f71b32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -40,15 +39,12 @@ public final class EventMessageEncoder { * @param eventMessage The event message to be encoded. * @return The serialized byte array. */ - @Nullable public byte[] encode(EventMessage eventMessage) { byteArrayOutputStream.reset(); try { writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri); String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; writeNullTerminatedString(dataOutputStream, nonNullValue); - writeUnsignedInt(dataOutputStream, 1000); // timescale - writeUnsignedInt(dataOutputStream, 0); // presentation_time_delta writeUnsignedInt(dataOutputStream, eventMessage.durationMs); writeUnsignedInt(dataOutputStream, eventMessage.id); dataOutputStream.write(eventMessage.messageData); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index d870afac3a..88a61d0bce 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -30,18 +32,19 @@ public final class EventMessageDecoderTest { @Test public void testDecodeEventMessage() { - byte[] rawEmsgBody = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48000 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + byte[] rawEmsgBody = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} EventMessageDecoder decoder = new EventMessageDecoder(); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); + Metadata metadata = decoder.decode(buffer); + assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); assertThat(eventMessage.schemeIdUri).isEqualTo("urn:test"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index ca8303d3e2..56830035cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -29,67 +31,52 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class EventMessageEncoderTest { + private static final EventMessage DECODED_MESSAGE = + new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); + + private static final byte[] ENCODED_MESSAGE = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} + @Test public void testEncodeEventStream() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] foo = new byte[] {1, 2, 3}; + + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); } @Test public void testEncodeDecodeEventStream() throws IOException { - EventMessage expectedEmsg = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg); + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); EventMessageDecoder decoder = new EventMessageDecoder(); Metadata metadata = decoder.decode(buffer); assertThat(metadata.length()).isEqualTo(1); - assertThat(metadata.get(0)).isEqualTo(expectedEmsg); + assertThat(metadata.get(0)).isEqualTo(DECODED_MESSAGE); } @Test public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, new byte[] {4, 3, 2, 1, 0}); byte[] expectedEmsgBody1 = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -46, // id = 1000402 - 4, 3, 2, 1, 0 - }; // message_data = {4, 3, 2, 1, 0} + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 210), // id = 1000402 + createByteArray(4, 3, 2, 1, 0)); // message_data = {4, 3, 2, 1, 0} + EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] encodedByteArray = eventMessageEncoder.encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1); assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); } From 921ff02c90a1c43477bb234058c83da37174b67a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Aug 2019 16:56:57 +0100 Subject: [PATCH 221/424] Only read from FormatHolder when a format has been read I think we need to start clearing the holder as part of the DRM rework. When we do this, it'll only be valid to read from the holder immediately after it's been populated. PiperOrigin-RevId: 262362725 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 5 ++++- 2 files changed, 5 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 d5da9a011d..01ee673442 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 @@ -844,7 +844,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { pendingFormat = null; } inputBuffer.flip(); - inputBuffer.colorInfo = formatHolder.format.colorInfo; + inputBuffer.colorInfo = format.colorInfo; onQueueInputBuffer(inputBuffer); decoder.queueInputBuffer(inputBuffer); buffersInCodecCount++; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index d360224872..6373510154 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -58,6 +58,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private int pendingMetadataCount; private MetadataDecoder decoder; private boolean inputStreamEnded; + private long subsampleOffsetUs; /** * @param output The output. @@ -126,7 +127,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; Metadata metadata = decoder.decode(buffer); @@ -136,6 +137,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount++; } } + } else if (result == C.RESULT_FORMAT_READ) { + subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; } } From 4656196daeb223ff3b64335ef3268c52c6539b26 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Aug 2019 08:33:42 +0100 Subject: [PATCH 222/424] Upgrade IMA dependency version PiperOrigin-RevId: 262511088 --- extensions/ima/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 0ef9f281c9..f51c4f954f 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' From a381cbf5362c2a2e44c52c36a0330a0c29758f97 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Aug 2019 10:43:26 +0100 Subject: [PATCH 223/424] Make reset on network change the default. PiperOrigin-RevId: 262886490 --- RELEASENOTES.md | 1 + .../android/exoplayer2/upstream/DefaultBandwidthMeter.java | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 133f68195d..1f3cf58247 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ `DefaultTrackSelector` to allow adaptive selections of audio tracks with different channel counts ([#6257](https://github.com/google/ExoPlayer/issues/6257)). +* Reset `DefaultBandwidthMeter` to initial values on network change. ### 2.10.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 76515a98e6..1f306dd69d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -100,6 +100,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context)); slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT; clock = Clock.DEFAULT; + resetOnNetworkTypeChange = true; } /** @@ -168,14 +169,12 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } /** - * Sets whether to reset if the network type changes. - * - *

    This method is experimental, and will be renamed or removed in a future release. + * Sets whether to reset if the network type changes. The default value is {@code true}. * * @param resetOnNetworkTypeChange Whether to reset if the network type changes. * @return This builder. */ - public Builder experimental_resetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) { + public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) { this.resetOnNetworkTypeChange = resetOnNetworkTypeChange; return this; } From 90b62c67fbf29bff2406d16979539da4ec126166 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Aug 2019 13:09:21 +0100 Subject: [PATCH 224/424] Change default video buffer size to 32MB. The current max video buffer is 13MB which is too small for high quality streams and doesn't allow the DefaultLoadControl to buffer up to its default max buffer time of 50 seconds. Also move util method and constants only used by DefaultLoadControl into this class. PiperOrigin-RevId: 263328088 --- RELEASENOTES.md | 2 + .../java/com/google/android/exoplayer2/C.java | 19 --------- .../exoplayer2/DefaultLoadControl.java | 42 ++++++++++++++++++- .../google/android/exoplayer2/util/Util.java | 29 ------------- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1f3cf58247..61d0c94344 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ different channel counts ([#6257](https://github.com/google/ExoPlayer/issues/6257)). * Reset `DefaultBandwidthMeter` to initial values on network change. +* Increase maximum buffer size for video in `DefaultLoadControl` to ensure high + quality video can be loaded up to the full default buffer duration. ### 2.10.4 ### 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 cf0f97ea76..56f9494856 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 @@ -671,25 +671,6 @@ public final class C { /** A default size in bytes for an individual allocation that forms part of a larger buffer. */ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - /** A default size in bytes for a video buffer. */ - public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for an audio buffer. */ - public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a text buffer. */ - public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a metadata buffer. */ - public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a camera motion buffer. */ - public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ - public static final int DEFAULT_MUXED_BUFFER_SIZE = - DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; - /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */ @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cenc = "cenc"; 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 972f651a41..1244b96d94 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 @@ -67,6 +67,25 @@ public class DefaultLoadControl implements LoadControl { /** The default for whether the back buffer is retained from the previous keyframe. */ public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false; + /** A default size in bytes for a video buffer. */ + public static final int DEFAULT_VIDEO_BUFFER_SIZE = 500 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for an audio buffer. */ + public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a text buffer. */ + public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a metadata buffer. */ + public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a camera motion buffer. */ + public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ + public static final int DEFAULT_MUXED_BUFFER_SIZE = + DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** Builder for {@link DefaultLoadControl}. */ public static final class Builder { @@ -404,7 +423,7 @@ public class DefaultLoadControl implements LoadControl { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { if (trackSelectionArray.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType()); } } return targetBufferSize; @@ -418,6 +437,27 @@ public class DefaultLoadControl implements LoadControl { } } + private static int getDefaultBufferSize(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_DEFAULT: + return DEFAULT_MUXED_BUFFER_SIZE; + case C.TRACK_TYPE_AUDIO: + return DEFAULT_AUDIO_BUFFER_SIZE; + case C.TRACK_TYPE_VIDEO: + return DEFAULT_VIDEO_BUFFER_SIZE; + case C.TRACK_TYPE_TEXT: + return DEFAULT_TEXT_BUFFER_SIZE; + case C.TRACK_TYPE_METADATA: + return DEFAULT_METADATA_BUFFER_SIZE; + case C.TRACK_TYPE_CAMERA_MOTION: + return DEFAULT_CAMERA_MOTION_BUFFER_SIZE; + case C.TRACK_TYPE_NONE: + return 0; + default: + throw new IllegalArgumentException(); + } + } + 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) { 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 095394b2f5..144a670294 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 @@ -1530,35 +1530,6 @@ public final class Util { : formatter.format("%02d:%02d", minutes, seconds).toString(); } - /** - * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} {@code - * DEFAULT_*_BUFFER_SIZE} constant. - * - * @param trackType The track type. - * @return The corresponding default buffer size in bytes. - * @throws IllegalArgumentException If the track type is an unrecognized or custom track type. - */ - public static int getDefaultBufferSize(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_DEFAULT: - return C.DEFAULT_MUXED_BUFFER_SIZE; - case C.TRACK_TYPE_AUDIO: - return C.DEFAULT_AUDIO_BUFFER_SIZE; - case C.TRACK_TYPE_VIDEO: - return C.DEFAULT_VIDEO_BUFFER_SIZE; - case C.TRACK_TYPE_TEXT: - return C.DEFAULT_TEXT_BUFFER_SIZE; - case C.TRACK_TYPE_METADATA: - return C.DEFAULT_METADATA_BUFFER_SIZE; - case C.TRACK_TYPE_CAMERA_MOTION: - return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE; - case C.TRACK_TYPE_NONE: - return 0; - default: - throw new IllegalArgumentException(); - } - } - /** * Escapes a string so that it's safe for use as a file or directory name on at least FAT32 * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today. From dcac4aa67f250dfaedf2a52fc808bd2d9e760cba Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 14:12:51 +0100 Subject: [PATCH 225/424] Add description to TextInformationFrame.toString() output This field is used in .equals(), we should print it in toString() too PiperOrigin-RevId: 263335432 --- .../android/exoplayer2/metadata/id3/TextInformationFrame.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 8a36276b91..5dd5280e78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -66,7 +66,7 @@ public final class TextInformationFrame extends Id3Frame { @Override public String toString() { - return id + ": value=" + value; + return id + ": description=" + description + ": value=" + value; } // Parcelable implementation. From 2de1a204e2be4918210052224f5b86dbed87f06b Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 14:13:32 +0100 Subject: [PATCH 226/424] Add Metadata.toString that prints the contents of `entries` entries are used in .equals(), so it's good to have them printed in toString() too (for test failures) and it makes logging easier too. PiperOrigin-RevId: 263335503 --- .../com/google/android/exoplayer2/metadata/Metadata.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 7b4f4c0836..dbc1114bd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -122,6 +122,11 @@ public final class Metadata implements Parcelable { return Arrays.hashCode(entries); } + @Override + public String toString() { + return "entries=" + Arrays.toString(entries); + } + // Parcelable implementation. @Override From 5100e67c831a279122bf03eb37b153a186dc6e41 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 15 Aug 2019 12:09:48 +0100 Subject: [PATCH 227/424] Support unwrapping nested Metadata messages in MetadataRenderer Initially this supports ID3-in-EMSG, but can also be used to support SCTE35-in-EMSG too. PiperOrigin-RevId: 263535925 --- .../android/exoplayer2/metadata/Metadata.java | 26 ++- .../exoplayer2/metadata/MetadataRenderer.java | 46 +++++- .../metadata/emsg/EventMessage.java | 22 +++ .../metadata/MetadataRendererTest.java | 153 ++++++++++++++++++ 4 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index dbc1114bd5..35702da576 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; @@ -28,10 +29,27 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ public final class Metadata implements Parcelable { - /** - * A metadata entry. - */ - public interface Entry extends Parcelable {} + /** A metadata entry. */ + public interface Entry extends Parcelable { + + /** + * Returns the {@link Format} that can be used to decode the wrapped metadata in {@link + * #getWrappedMetadataBytes()}, or null if this Entry doesn't contain wrapped metadata. + */ + @Nullable + default Format getWrappedMetadataFormat() { + return null; + } + + /** + * Returns the bytes of the wrapped metadata in this Entry, or null if it doesn't contain + * wrapped metadata. + */ + @Nullable + default byte[] getWrappedMetadataBytes() { + return null; + } + } private final Entry[] entries; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 6373510154..be965bd480 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -27,7 +27,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * A renderer for metadata. @@ -129,12 +131,18 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; Metadata metadata = decoder.decode(buffer); if (metadata != null) { - pendingMetadata[index] = metadata; - pendingMetadataTimestamps[index] = buffer.timeUs; - pendingMetadataCount++; + List entries = new ArrayList<>(metadata.length()); + decodeWrappedMetadata(metadata, entries); + if (!entries.isEmpty()) { + Metadata expandedMetadata = new Metadata(entries); + int index = + (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = expandedMetadata; + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; + } } } } else if (result == C.RESULT_FORMAT_READ) { @@ -150,6 +158,36 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } + /** + * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped + * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion + * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter). + */ + private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { + for (int i = 0; i < metadata.length(); i++) { + Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { + MetadataDecoder wrappedMetadataDecoder = + decoderFactory.createDecoder(wrappedMetadataFormat); + // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too. + byte[] wrappedMetadataBytes = + Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); + buffer.clear(); + buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); + buffer.data.put(wrappedMetadataBytes); + buffer.flip(); + @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); + if (innerMetadata != null) { + // The decoding succeeded, so we'll try another level of unwrapping. + decodeWrappedMetadata(innerMetadata, decodedEntries); + } + } else { + // Entry doesn't contain any wrapped metadata, so output it directly. + decodedEntries.add(metadata.get(i)); + } + } + } + @Override protected void onDisabled() { flushPendingMetadata(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index ca1e390181..c9e9d54093 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -20,7 +20,10 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -29,6 +32,13 @@ import java.util.Arrays; */ public final class EventMessage implements Metadata.Entry { + @VisibleForTesting + public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + + private static final Format ID3_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + /** * The message scheme. */ @@ -81,6 +91,18 @@ public final class EventMessage implements Metadata.Entry { messageData = castNonNull(in.createByteArray()); } + @Override + @Nullable + public Format getWrappedMetadataFormat() { + return ID3_SCHEME_ID.equals(schemeIdUri) ? ID3_FORMAT : null; + } + + @Override + @Nullable + public byte[] getWrappedMetadataBytes() { + return ID3_SCHEME_ID.equals(schemeIdUri) ? messageData : null; + } + @Override public int hashCode() { if (hashCode == 0) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java new file mode 100644 index 0000000000..4de8bb76cc --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -0,0 +1,153 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MetadataRenderer}. */ +@RunWith(AndroidJUnit4.class) +public class MetadataRendererTest { + + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + private final EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); + + @Test + public void decodeMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + "urn:test-scheme-id", + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + "Test data".getBytes(UTF_8)); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + assertThat(metadata.get(0).get(0)).isEqualTo(emsg); + } + + @Test + public void decodeMetadata_handlesWrappedMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + EventMessage.ID3_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + encodeTxxxId3Frame("Test description", "Test value")); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + TextInformationFrame expectedId3Frame = + new TextInformationFrame("TXXX", "Test description", "Test value"); + assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); + } + + @Test + public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + EventMessage.ID3_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + "Not a real ID3 tag".getBytes(ISO_8859_1)); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).isEmpty(); + } + + private static List runRenderer(byte[] input) throws ExoPlaybackException { + List metadata = new ArrayList<>(); + MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); + renderer.replaceStream( + new Format[] {EMSG_FORMAT}, + new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), + /* offsetUs= */ 0L); + renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format + renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data + + return Collections.unmodifiableList(metadata); + } + + /** + * Builds an ID3v2 tag containing a single 'user defined text information frame' (id='TXXX') with + * {@code description} and {@code value}. + * + *

    + */ + private static byte[] encodeTxxxId3Frame(String description, String value) { + byte[] id3FrameData = + TestUtil.joinByteArrays( + "TXXX".getBytes(ISO_8859_1), // ID for a 'user defined text information frame' + TestUtil.createByteArray(0, 0, 0, 0), // Frame size (set later) + TestUtil.createByteArray(0, 0), // Frame flags + TestUtil.createByteArray(0), // Character encoding = ISO-8859-1 + description.getBytes(ISO_8859_1), + TestUtil.createByteArray(0), // String null terminator + value.getBytes(ISO_8859_1), + TestUtil.createByteArray(0)); // String null terminator + int frameSizeIndex = 7; + int frameSize = id3FrameData.length - 10; + Assertions.checkArgument( + frameSize < 128, "frameSize must fit in 7 bits to avoid synch-safe encoding: " + frameSize); + id3FrameData[frameSizeIndex] = (byte) frameSize; + + byte[] id3Bytes = + TestUtil.joinByteArrays( + "ID3".getBytes(ISO_8859_1), // identifier + TestUtil.createByteArray(0x04, 0x00), // version + TestUtil.createByteArray(0), // Tag flags + TestUtil.createByteArray(0, 0, 0, 0), // Tag size (set later) + id3FrameData); + int tagSizeIndex = 9; + int tagSize = id3Bytes.length - 10; + Assertions.checkArgument( + tagSize < 128, "tagSize must fit in 7 bits to avoid synch-safe encoding: " + tagSize); + id3Bytes[tagSizeIndex] = (byte) tagSize; + return id3Bytes; + } +} From 08bb42ddc5bea8e6193f54024b90a5ce386a373a Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Fri, 16 Aug 2019 10:37:48 +0530 Subject: [PATCH 228/424] Upgrade librtmp-client to 3.1.0 --- extensions/rtmp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index b74be659ee..ba63843043 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'net.butterflytv.utils:rtmp-client:3.0.1' + implementation 'net.butterflytv.utils:rtmp-client:3.1.0' implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From 9ec346a2e181360bacef4903a1ed33c2d2a340d6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:31:05 +0100 Subject: [PATCH 229/424] Modify EventMessageDecoder to return null if decoding fails (currently throws exceptions) This matches the documentation on MetadataDecoder.decode: "@return The decoded metadata object, or null if the metadata could not be decoded." PiperOrigin-RevId: 263767144 --- .../metadata/emsg/EventMessageDecoder.java | 29 +++++++++++++------ .../metadata/MetadataRendererTest.java | 20 +++++++++---- .../source/dash/PlayerEmsgHandler.java | 3 ++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index f592a6eee7..d4e254f956 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -28,21 +29,31 @@ public final class EventMessageDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); - return new Metadata(decode(new ParsableByteArray(data, size))); + EventMessage decodedEventMessage = decode(new ParsableByteArray(data, size)); + if (decodedEventMessage == null) { + return null; + } else { + return new Metadata(decodedEventMessage); + } } + @Nullable public EventMessage decode(ParsableByteArray emsgData) { - String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long durationMs = emsgData.readUnsignedInt(); - long id = emsgData.readUnsignedInt(); - byte[] messageData = - Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + try { + String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + long durationMs = emsgData.readUnsignedInt(); + long id = emsgData.readUnsignedInt(); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } catch (RuntimeException e) { + return null; + } } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index 4de8bb76cc..26dcefc611 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -55,13 +55,20 @@ public class MetadataRendererTest { /* id= */ 0, "Test data".getBytes(UTF_8)); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); assertThat(metadata.get(0).get(0)).isEqualTo(emsg); } + @Test + public void decodeMetadata_skipsMalformed() throws Exception { + List metadata = runRenderer(EMSG_FORMAT, "not valid emsg bytes".getBytes(UTF_8)); + + assertThat(metadata).isEmpty(); + } + @Test public void decodeMetadata_handlesWrappedMetadata() throws Exception { EventMessage emsg = @@ -72,7 +79,7 @@ public class MetadataRendererTest { /* id= */ 0, encodeTxxxId3Frame("Test description", "Test value")); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); @@ -91,17 +98,18 @@ public class MetadataRendererTest { /* id= */ 0, "Not a real ID3 tag".getBytes(ISO_8859_1)); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).isEmpty(); } - private static List runRenderer(byte[] input) throws ExoPlaybackException { + private static List runRenderer(Format format, byte[] input) + throws ExoPlaybackException { List metadata = new ArrayList<>(); MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); renderer.replaceStream( - new Format[] {EMSG_FORMAT}, - new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), + new Format[] {format}, + new FakeSampleStream(format, /* eventDispatcher= */ null, input), /* offsetUs= */ 0L); renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 34e1ecc2b6..d11ccdecec 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -360,6 +360,9 @@ public final class PlayerEmsgHandler implements Handler.Callback { } long eventTimeUs = inputBuffer.timeUs; Metadata metadata = decoder.decode(inputBuffer); + if (metadata == null) { + continue; + } EventMessage eventMessage = (EventMessage) metadata.get(0); if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { parsePlayerEmsgEvent(eventTimeUs, eventMessage); From d3d192e36e8644086c6243b56dfbbd9aec34a95b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:39:57 +0100 Subject: [PATCH 230/424] Extend EventMessage.toString to include durationMs This field is used in .equals(), so it makes sense to include it in toString() too. PiperOrigin-RevId: 263768329 --- .../android/exoplayer2/metadata/emsg/EventMessage.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index c9e9d54093..7d35a15e31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -135,7 +135,14 @@ public final class EventMessage implements Metadata.Entry { @Override public String toString() { - return "EMSG: scheme=" + schemeIdUri + ", id=" + id + ", value=" + value; + return "EMSG: scheme=" + + schemeIdUri + + ", id=" + + id + + ", durationMs=" + + durationMs + + ", value=" + + value; } // Parcelable implementation. From 47e0580d80bd9276aa2d377bb0672982921a1c3a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:40:43 +0100 Subject: [PATCH 231/424] Unwrap SCTE-35 messages in emsg boxes PiperOrigin-RevId: 263768428 --- .../metadata/emsg/EventMessage.java | 25 ++++++++--- .../metadata/MetadataRendererTest.java | 43 ++++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 7d35a15e31..6e0b0b40f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -35,13 +35,21 @@ public final class EventMessage implements Metadata.Entry { @VisibleForTesting public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + /** + * scheme_id_uri from section 7.3.2 of SCTE 214-3 + * 2015. + */ + @VisibleForTesting public static final String SCTE35_SCHEME_ID = "urn:scte:scte35:2014:bin"; + private static final Format ID3_FORMAT = Format.createSampleFormat( /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + private static final Format SCTE35_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE); - /** - * The message scheme. - */ + /** The message scheme. */ public final String schemeIdUri; /** @@ -94,13 +102,20 @@ public final class EventMessage implements Metadata.Entry { @Override @Nullable public Format getWrappedMetadataFormat() { - return ID3_SCHEME_ID.equals(schemeIdUri) ? ID3_FORMAT : null; + switch (schemeIdUri) { + case ID3_SCHEME_ID: + return ID3_FORMAT; + case SCTE35_SCHEME_ID: + return SCTE35_FORMAT; + default: + return null; + } } @Override @Nullable public byte[] getWrappedMetadataBytes() { - return ID3_SCHEME_ID.equals(schemeIdUri) ? messageData : null; + return getWrappedMetadataFormat() != null ? messageData : null; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index 26dcefc611..af6489f726 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.metadata.scte35.TimeSignalCommand; import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Assertions; @@ -40,6 +41,28 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MetadataRendererTest { + private static final byte[] SCTE35_TIME_SIGNAL_BYTES = + TestUtil.joinByteArrays( + TestUtil.createByteArray( + 0, // table_id. + 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x14, // section_length(8). + 0x00, // protocol_version. + 0x00), // encrypted_packet, encryption_algorithm, pts_adjustment(1). + TestUtil.createByteArray(0x00, 0x00, 0x00, 0x00), // pts_adjustment(32). + TestUtil.createByteArray( + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x05, // splice_command_length(8). + 0x06, // splice_command_type = time_signal. + // Start of splice_time(). + 0x80), // time_specified_flag, reserved, pts_time(1). + TestUtil.createByteArray( + 0x52, 0x03, 0x02, 0x8f), // pts_time(32). PTS for a second after playback position. + TestUtil.createByteArray( + 0x00, 0x00, 0x00, 0x00)); // CRC_32 (ignored, check happens at extraction). + private static final Format EMSG_FORMAT = Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); @@ -70,7 +93,7 @@ public class MetadataRendererTest { } @Test - public void decodeMetadata_handlesWrappedMetadata() throws Exception { + public void decodeMetadata_handlesId3WrappedInEmsg() throws Exception { EventMessage emsg = new EventMessage( EventMessage.ID3_SCHEME_ID, @@ -88,6 +111,24 @@ public class MetadataRendererTest { assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); } + @Test + public void decodeMetadata_handlesScte35WrappedInEmsg() throws Exception { + + EventMessage emsg = + new EventMessage( + EventMessage.SCTE35_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + SCTE35_TIME_SIGNAL_BYTES); + + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + assertThat(metadata.get(0).get(0)).isInstanceOf(TimeSignalCommand.class); + } + @Test public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { EventMessage emsg = From c60b355f9c508ee5fa85f4169f45d38aed4ea27e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 19 Aug 2019 12:03:55 +0100 Subject: [PATCH 232/424] Add support for the AOM scheme_id for ID3-in-EMSG https://developer.apple.com/documentation/http_live_streaming/about_the_common_media_application_format_with_http_live_streaming PiperOrigin-RevId: 264126140 --- .../metadata/emsg/EventMessage.java | 20 +++++++++++++------ .../metadata/MetadataRendererTest.java | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 6e0b0b40f2..7e3862ca31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -27,13 +27,20 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; -/** - * An Event Message (emsg) as defined in ISO 23009-1. - */ +/** An Event Message (emsg) as defined in ISO 23009-1. */ public final class EventMessage implements Metadata.Entry { - @VisibleForTesting - public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + /** + * emsg scheme_id_uri from the CMAF + * spec. + */ + @VisibleForTesting public static final String ID3_SCHEME_ID_AOM = "https://aomedia.org/emsg/ID3"; + + /** + * The Apple-hosted scheme_id equivalent to {@code ID3_SCHEME_ID_AOM} - used before AOM adoption. + */ + private static final String ID3_SCHEME_ID_APPLE = + "https://developer.apple.com/streaming/emsg-id3"; /** * scheme_id_uri from section 7.3.2 of Date: Tue, 20 Aug 2019 08:40:44 +0100 Subject: [PATCH 233/424] Fix handling of delayed AdsLoader.start AdsMediaSource posts AdsLoader.start to the main thread during preparation, but the app may call AdsLoader.setPlayer(null) before it actually runs (e.g., if initializing then quickly backgrounding the player). This is valid usage of the API so handle this case instead of asserting. Because not calling setPlayer at all is a pitfall of the API, track whether setPlayer has been called and still assert that in AdsLoader.start. PiperOrigin-RevId: 264329632 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 ++++++++++-- 1 file changed, 10 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 68e48b8d33..11071a7e4b 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 @@ -327,6 +327,7 @@ public final class ImaAdsLoader private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; private Object pendingAdRequestContext; private List supportedMimeTypes; @@ -558,6 +559,7 @@ public final class ImaAdsLoader Assertions.checkState( player == null || player.getApplicationLooper() == Looper.getMainLooper()); nextPlayer = player; + wasSetPlayerCalled = true; } @Override @@ -585,9 +587,12 @@ public final class ImaAdsLoader @Override public void start(EventListener eventListener, AdViewProvider adViewProvider) { - Assertions.checkNotNull( - nextPlayer, "Set player using adsLoader.setPlayer before preparing the player."); + Assertions.checkState( + wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); player = nextPlayer; + if (player == null) { + return; + } this.eventListener = eventListener; lastVolumePercentage = 0; lastAdProgress = null; @@ -617,6 +622,9 @@ public final class ImaAdsLoader @Override public void stop() { + if (player == null) { + return; + } if (adsManager != null && imaPausedContent) { adPlaybackState = adPlaybackState.withAdResumePositionUs( From 9e3bee89e1f1a67f189d81b718df7dfc86541dfb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 21 Aug 2019 15:22:47 +0100 Subject: [PATCH 234/424] Prevent NPE in ImaAdsLoader onPositionDiscontinuity. Any seek before the first timeline becomes available will result in a NPE. Change it to handle that case gracefully. Issue:#5831 PiperOrigin-RevId: 264603061 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 11071a7e4b..f5ec9f120d 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 @@ -484,6 +484,7 @@ public final class ImaAdsLoader pendingContentPositionMs = C.TIME_UNSET; adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; + timeline = Timeline.EMPTY; } /** @@ -967,7 +968,7 @@ public final class ImaAdsLoader if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } - updateImaStateForPlayerState(); + onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @Override @@ -1022,7 +1023,7 @@ public final class ImaAdsLoader } } updateAdPlaybackState(); - } else { + } else if (!timeline.isEmpty()) { long positionMs = player.getCurrentPosition(); timeline.getPeriod(0, period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); @@ -1034,9 +1035,8 @@ public final class ImaAdsLoader } } } - } else { - updateImaStateForPlayerState(); } + updateImaStateForPlayerState(); } // Internal methods. From 886fe910a88ec32997472fbec3248d558a52a47c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 22 Aug 2019 08:44:39 +0100 Subject: [PATCH 235/424] Avoid potential ArrayStoreException with audio processors The app is able to pass a more specialized array type, so the Arrays.copyOf call produces an array into which it's not valid to store arbitrary AudioProcessors. Create a new array and copy into it to avoid this problem. PiperOrigin-RevId: 264779164 --- .../android/exoplayer2/audio/DefaultAudioSink.java | 11 +++++++++-- .../exoplayer2/audio/DefaultAudioSinkTest.java | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) 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 be1b7d3d53..6635ec40ac 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 @@ -37,7 +37,6 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; /** @@ -122,7 +121,15 @@ public final class DefaultAudioSink implements AudioSink { * audioProcessors} applied before silence skipping and playback parameters. */ public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { - this.audioProcessors = Arrays.copyOf(audioProcessors, audioProcessors.length + 2); + // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array + // rather than using Arrays.copyOf. + this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; + System.arraycopy( + /* src= */ audioProcessors, + /* srcPos= */ 0, + /* dest= */ this.audioProcessors, + /* destPos= */ 0, + /* length= */ audioProcessors.length); silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java index d41c99183d..7adf618366 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java @@ -67,6 +67,13 @@ public final class DefaultAudioSinkTest { /* enableConvertHighResIntPcmToFloat= */ false); } + @Test + public void handlesSpecializedAudioProcessorArray() { + defaultAudioSink = + new DefaultAudioSink( + AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, new TeeAudioProcessor[0]); + } + @Test public void handlesBufferAfterReset() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); From 7cefb56eda56d285d6b4cc8a3c96475f9a18487b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 23 Aug 2019 09:57:26 +0100 Subject: [PATCH 236/424] Update comment to indicate correct int value of "FLAG_ALLOW_CACHE_FRAGMENTATION" in ExoPlayer2 upstream DataSpec Currently the value of FLAG_ALLOW_CACHE_FRAGMENTATION is defined as "1 << 4" but commented as "8". Either the value of FLAG_ALLOW_CACHE_FRAGMENTATION should be "1 << 3", or the comment should be 16. Here I am modifying the comment since it does not affect any current behavior. PiperOrigin-RevId: 265011839 --- .../java/com/google/android/exoplayer2/upstream/DataSpec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a98f773c9d..e32e063d0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -68,7 +68,7 @@ public final class DataSpec { * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment * whilst writing another). */ - public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 4; // 8 + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 4; // 16 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link From 0085a7e761130b6a00d17546463e7e8c67fd0669 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 23 Aug 2019 16:16:07 +0100 Subject: [PATCH 237/424] Defer adsManager.init until the timeline has loaded If the app seeks after we get an ads manager but before the player exposes the timeline with ads, we would end up expecting to play a preroll even after the seek request arrived. This caused the player to get stuck. Wait until a non-empty timeline has been exposed via onTimelineChanged before initializing IMA (at which point it can start polling the player position). Seek requests are not handled while an ad is playing. PiperOrigin-RevId: 265058325 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 20 +++++++++++-------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 ++- 2 files changed, 14 insertions(+), 9 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 f5ec9f120d..335f8374dd 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 @@ -338,6 +338,7 @@ public final class ImaAdsLoader private int lastVolumePercentage; private AdsManager adsManager; + private boolean initializedAdsManager; private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; @@ -613,8 +614,8 @@ public final class ImaAdsLoader adsManager.resume(); } } else if (adsManager != null) { - // Ads have loaded but the ads manager is not initialized. - startAdPlayback(); + adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + updateAdPlaybackState(); } else { // Ads haven't loaded yet, so request them. requestAds(adViewGroup); @@ -688,7 +689,8 @@ public final class ImaAdsLoader if (player != null) { // If a player is attached already, start playback immediately. try { - startAdPlayback(); + adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("onAdsManagerLoaded", e); } @@ -968,6 +970,10 @@ public final class ImaAdsLoader if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } + if (!initializedAdsManager && adsManager != null) { + initializedAdsManager = true; + initializeAdsManager(); + } onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @@ -1041,7 +1047,7 @@ public final class ImaAdsLoader // Internal methods. - private void startAdPlayback() { + private void initializeAdsManager() { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); @@ -1056,10 +1062,9 @@ public final class ImaAdsLoader adsRenderingSettings.setUiElements(adUiElements); } - // Set up the ad playback state, skipping ads based on the start position as required. + // Skip ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - long contentPositionMs = player.getCurrentPosition(); + long contentPositionMs = player.getContentPosition(); int adGroupIndexForPosition = adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { @@ -1093,7 +1098,6 @@ public final class ImaAdsLoader pendingContentPositionMs = contentPositionMs; } - // Start ad playback. adsManager.init(adsRenderingSettings); updateAdPlaybackState(); if (DEBUG) { diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 1e1935c63a..4b2020a7d5 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -143,7 +143,8 @@ public class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( new AdPlaybackState(/* adGroupTimesUs= */ 0) - .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)); + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withContentDurationUs(CONTENT_DURATION_US)); } @Test From 46bf710cb34a5c4491ca4d8578cdc2915813df67 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 23 Aug 2019 16:57:14 +0100 Subject: [PATCH 238/424] Do not compare bitrates of audio tracks with different languages. The last selection criteria is the audio bitrate to prefer higher-quality streams. We shouldn't apply this criterium though if the languages of the tracks are different. Issue:#6335 PiperOrigin-RevId: 265064756 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 8 +- .../DefaultTrackSelectorTest.java | 91 ++++++++++++++----- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 61d0c94344..17664dbe53 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * Reset `DefaultBandwidthMeter` to initial values on network change. * Increase maximum buffer size for video in `DefaultLoadControl` to ensure high quality video can be loaded up to the full default buffer duration. +* Fix audio selection issue where languages are compared by bit rate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). ### 2.10.4 ### 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 762e0a98b4..de4415ce39 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 @@ -2501,6 +2501,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean isWithinConstraints; + @Nullable private final String language; private final Parameters parameters; private final boolean isWithinRendererCapabilities; private final int preferredLanguageScore; @@ -2513,6 +2514,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { this.parameters = parameters; + this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredAudioLanguage); isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; @@ -2580,7 +2582,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.sampleRate != other.sampleRate) { return resultSign * compareInts(this.sampleRate, other.sampleRate); } - return resultSign * compareInts(this.bitrate, other.bitrate); + if (Util.areEqual(this.language, other.language)) { + // Only compare bit rates of tracks with the same or unknown language. + return resultSign * compareInts(this.bitrate, other.bitrate); + } + return 0; } } 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 9941ae1098..92c4628fa0 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 @@ -719,37 +719,38 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will select audio tracks with higher bit-rate when other factors are - * the same, and tracks are within renderer's capabilities. + * Tests that track selector will select audio tracks with higher bit rate when other factors are + * the same, and tracks are within renderer's capabilities, and have the same language. */ @Test - public void testSelectTracksWithinCapabilitiesSelectHigherBitrate() throws Exception { + public void selectAudioTracks_withinCapabilities_andSameLanguage_selectsHigherBitrate() + throws Exception { Format lowerBitrateFormat = Format.createAudioSampleFormat( "audioFormat", MimeTypes.AUDIO_AAC, - null, - 15000, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null); + /* codecs= */ null, + /* bitrate= */ 15000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); Format higherBitrateFormat = Format.createAudioSampleFormat( "audioFormat", MimeTypes.AUDIO_AAC, - null, - 30000, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null); + /* codecs= */ null, + /* bitrate= */ 30000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat); TrackSelectorResult result = @@ -761,14 +762,58 @@ public final class DefaultTrackSelectorTest { assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat); } + /** + * Tests that track selector will select the first audio track even if other tracks with a + * different language have higher bit rates, all other factors are the same, and tracks are within + * renderer's capabilities. + */ + @Test + public void selectAudioTracks_withinCapabilities_andDifferentLanguage_selectsFirstTrack() + throws Exception { + Format firstLanguageFormat = + Format.createAudioSampleFormat( + "audioFormat", + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ 15000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); + Format higherBitrateFormat = + Format.createAudioSampleFormat( + "audioFormat", + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ 30000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "te"); + TrackGroupArray trackGroups = wrapFormats(firstLanguageFormat, higherBitrateFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + trackGroups, + periodId, + TIMELINE); + assertFixedSelection(result.selections.get(0), trackGroups, firstLanguageFormat); + } + /** * Tests that track selector will prefer audio tracks with higher channel count over tracks with * higher sample rate when other factors are the same, and tracks are within renderer's * capabilities. */ @Test - public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() - throws Exception { + public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { Format higherChannelLowerSampleRateFormat = Format.createAudioSampleFormat( "audioFormat", From c3d6be3afdd7c0ca68dba15e443bc64aa3f61073 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 23 Aug 2019 18:48:14 +0100 Subject: [PATCH 239/424] Add HTTP request parameters (headers) to DataSpec. Adds HTTP request parameters in DataSpec. Keeps DataSpec behavior to be immutable as before. PiperOrigin-RevId: 265087782 --- .../android/exoplayer2/upstream/DataSpec.java | 60 +++++++- .../exoplayer2/upstream/DataSpecTest.java | 128 ++++++++++++++++++ 2 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index e32e063d0c..3563078c87 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -24,6 +24,9 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Defines a region of data. @@ -102,9 +105,10 @@ public final class DataSpec { /** @deprecated Use {@link #httpBody} instead. */ @Deprecated public final @Nullable byte[] postBody; - /** - * The absolute position of the data in the full stream. - */ + /** Immutable map containing the headers to use in HTTP requests. */ + public final Map httpRequestHeaders; + + /** The absolute position of the data in the full stream. */ public final long absoluteStreamPosition; /** * The position of the data when read from {@link #uri}. @@ -235,7 +239,6 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - @SuppressWarnings("deprecation") public DataSpec( Uri uri, @HttpMethod int httpMethod, @@ -245,6 +248,41 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { + this( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + /* httpRequestHeaders= */ Collections.emptyMap()); + } + + /** + * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); @@ -257,6 +295,7 @@ public final class DataSpec { this.length = length; this.key = key; this.flags = flags; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -344,7 +383,8 @@ public final class DataSpec { position + offset, length, key, - flags); + flags, + httpRequestHeaders); } } @@ -356,6 +396,14 @@ public final class DataSpec { */ public DataSpec withUri(Uri uri) { return new DataSpec( - uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + httpRequestHeaders); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java new file mode 100644 index 0000000000..f6e30f814a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java @@ -0,0 +1,128 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link DataSpec}. */ +@RunWith(AndroidJUnit4.class) +public class DataSpecTest { + + @Test + public void createDataSpec_withDefaultValues_setsEmptyHttpRequestParameters() { + Uri uri = Uri.parse("www.google.com"); + DataSpec dataSpec = new DataSpec(uri); + + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + + dataSpec = new DataSpec(uri, /*flags= */ 0); + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + + dataSpec = + new DataSpec( + uri, + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0); + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + } + + @Test + public void createDataSpec_setsHttpRequestParameters() { + Map httpRequestParameters = new HashMap<>(); + httpRequestParameters.put("key1", "value1"); + httpRequestParameters.put("key2", "value2"); + httpRequestParameters.put("key3", "value3"); + + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + httpRequestParameters); + + assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestParameters); + } + + @Test + public void httpRequestParameters_areReadOnly() { + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + /* httpRequestHeaders= */ new HashMap<>()); + + try { + dataSpec.httpRequestHeaders.put("key", "value"); + fail(); + } catch (UnsupportedOperationException expected) { + // Expected + } + } + + @Test + public void copyMethods_copiesHttpRequestHeaders() { + Map httpRequestParameters = new HashMap<>(); + httpRequestParameters.put("key1", "value1"); + httpRequestParameters.put("key2", "value2"); + httpRequestParameters.put("key3", "value3"); + + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + httpRequestParameters); + + DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + + dataSpecCopy = dataSpec.subrange(2); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + + dataSpecCopy = dataSpec.subrange(2, 2); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + } +} From ad699b8ff665b013ea3181662ecfc8f334635a91 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Aug 2019 13:28:07 +0100 Subject: [PATCH 240/424] seenCacheError should be set for all errors PiperOrigin-RevId: 265662686 --- .../exoplayer2/upstream/cache/CacheDataSource.java | 9 ++++++--- .../android/exoplayer2/upstream/cache/CacheUtil.java | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) 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 e5df8d55c3..12107e6111 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 @@ -283,7 +283,7 @@ public final class CacheDataSource implements DataSource { } openNextSource(false); return bytesRemaining; - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -325,6 +325,9 @@ public final class CacheDataSource implements DataSource { } handleBeforeThrow(e); throw e; + } catch (Throwable e) { + handleBeforeThrow(e); + throw e; } } @@ -349,7 +352,7 @@ public final class CacheDataSource implements DataSource { notifyBytesRead(); try { closeCurrentSource(); - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -516,7 +519,7 @@ public final class CacheDataSource implements DataSource { } } - private void handleBeforeThrow(IOException exception) { + private void handleBeforeThrow(Throwable exception) { if (isReadingFromCache() || exception instanceof CacheException) { seenCacheError = true; } 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 47470c5de7..6277ec686f 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 @@ -359,7 +359,7 @@ public final class CacheUtil { } } - /*package*/ static boolean isCausedByPositionOutOfRange(IOException e) { + /* package */ static boolean isCausedByPositionOutOfRange(IOException e) { Throwable cause = e; while (cause != null) { if (cause instanceof DataSourceException) { From 407dbf075eeddda4c51b9bf84ac8d2a6129eafcf Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Aug 2019 21:41:19 +0100 Subject: [PATCH 241/424] Add HttpDataSource.getResponseCode to provide the status code associated with the most recent HTTP response. PiperOrigin-RevId: 266218104 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ext/cronet/CronetDataSource.java | 7 +++++++ .../android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 5 +++++ .../android/exoplayer2/upstream/DefaultHttpDataSource.java | 7 ++++++- .../google/android/exoplayer2/upstream/HttpDataSource.java | 6 ++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 17664dbe53..89ec97dbc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ quality video can be loaded up to the full default buffer duration. * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Add `HttpDataSource.getResponseCode` to provide the status code associated + with the most recent HTTP response. ### 2.10.4 ### 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 ca196b1d2f..0f94698e7a 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 @@ -299,6 +299,13 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { requestProperties.clear(); } + @Override + public int getResponseCode() { + return responseInfo == null || responseInfo.getHttpStatusCode() <= 0 + ? -1 + : responseInfo.getHttpStatusCode(); + } + @Override public Map> getResponseHeaders() { return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders(); 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 8eb8bba920..95bd4f71de 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 @@ -133,6 +133,11 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { return response == null ? null : Uri.parse(response.request().url().toString()); } + @Override + public int getResponseCode() { + return response == null ? -1 : response.code(); + } + @Override public Map> getResponseHeaders() { return response == null ? Collections.emptyMap() : response.headers().toMultimap(); 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 66036b7a84..87f95a32a2 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 @@ -82,6 +82,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private @Nullable HttpURLConnection connection; private @Nullable InputStream inputStream; private boolean opened; + private int responseCode; private long bytesToSkip; private long bytesToRead; @@ -252,6 +253,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return connection == null ? null : Uri.parse(connection.getURL().toString()); } + @Override + public int getResponseCode() { + return connection == null || responseCode <= 0 ? -1 : responseCode; + } + @Override public Map> getResponseHeaders() { return connection == null ? Collections.emptyMap() : connection.getHeaderFields(); @@ -288,7 +294,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou dataSpec, HttpDataSourceException.TYPE_OPEN); } - int responseCode; String responseMessage; try { responseCode = connection.getResponseCode(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 07155ee2bc..97ece840ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -354,6 +354,12 @@ public interface HttpDataSource extends DataSource { */ void clearAllRequestProperties(); + /** + * When the source is open, returns the HTTP response status code associated with the last {@link + * #open} call. Otherwise, returns a negative value. + */ + int getResponseCode(); + @Override Map> getResponseHeaders(); } From f05d67b7c794ce96e5e89aee2d901a1908dd0250 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Aug 2019 17:35:29 +0100 Subject: [PATCH 242/424] Simplify androidTest manifests & fix links to use https PiperOrigin-RevId: 266396506 --- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 39b92aa217..6736ab4b16 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index 7f775f4d32..031960636d 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index 6ca2e7164a..4d0832d198 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index e6e874a27a..831ad47831 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index be71884846..b6c6064227 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -23,7 +23,7 @@ - From fe422dbde4b97cf00439010758567be2c0904d20 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 1 Sep 2019 22:02:38 +0100 Subject: [PATCH 243/424] Merge pull request #6303 from ittiam-systems:rtmp-3.1.0 PiperOrigin-RevId: 266407058 --- RELEASENOTES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89ec97dbc9..79d4314d87 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,11 @@ ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. +* Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues + ([#4200](https://github.com/google/ExoPlayer/issues/4200), + [#4249](https://github.com/google/ExoPlayer/issues/4249), + [#4319](https://github.com/google/ExoPlayer/issues/4319), + [#4337](https://github.com/google/ExoPlayer/issues/4337)). ### 2.10.4 ### From 284a672bb325b67a330dda65f1093e7e033719e9 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 2 Sep 2019 13:43:42 +0100 Subject: [PATCH 244/424] Bypass sniffing for single extractor Sniffing is performed in ProgressiveMediaPeriod even if a single extractor is provided. Skip it in that case to improve performances. Issue:#6325 PiperOrigin-RevId: 266766373 --- RELEASENOTES.md | 2 ++ .../source/ProgressiveMediaPeriod.java | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 79d4314d87..7d4ca0fbe0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * Reset `DefaultBandwidthMeter` to initial values on network change. * Increase maximum buffer size for video in `DefaultLoadControl` to ensure high quality video can be loaded up to the full default buffer duration. +* Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is + provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Add `HttpDataSource.getResponseCode` to provide the status code associated diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 4dafa0ba76..4ec29bfb46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -1042,21 +1042,28 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (extractor != null) { return extractor; } - for (Extractor extractor : extractors) { - try { - if (extractor.sniff(input)) { - this.extractor = extractor; - break; + if (extractors.length == 1) { + this.extractor = extractors[0]; + } else { + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(input)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); } - } catch (EOFException e) { - // Do nothing. - } finally { - input.resetPeekPosition(); } - } - if (extractor == null) { - throw new UnrecognizedInputFormatException("None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); + if (extractor == null) { + throw new UnrecognizedInputFormatException( + "None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + + ") could read the stream.", + uri); + } } extractor.init(output); return extractor; From 4712bcfd5324cc8db5f401ea62109b26277a91f9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 2 Sep 2019 16:05:31 +0100 Subject: [PATCH 245/424] use isPlaying to determine which notification action to display in compact view PiperOrigin-RevId: 266782250 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerNotificationManager.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7d4ca0fbe0..d3cbc6b2d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ quality video can be loaded up to the full default buffer duration. * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon when + playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Add `HttpDataSource.getResponseCode` to provide the status code associated 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 260fb9d398..7fa4b60314 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 @@ -1182,10 +1182,10 @@ public class PlayerNotificationManager { if (skipPreviousActionIndex != -1) { actionIndices[actionCounter++] = skipPreviousActionIndex; } - boolean playWhenReady = player.getPlayWhenReady(); - if (pauseActionIndex != -1 && playWhenReady) { + boolean isPlaying = isPlaying(player); + if (pauseActionIndex != -1 && isPlaying) { actionIndices[actionCounter++] = pauseActionIndex; - } else if (playActionIndex != -1 && !playWhenReady) { + } else if (playActionIndex != -1 && !isPlaying) { actionIndices[actionCounter++] = playActionIndex; } if (skipNextActionIndex != -1) { From 525d0320a7c213a100feef8695bc469835eed276 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Sep 2019 16:56:32 -0700 Subject: [PATCH 246/424] Fix exception message PiperOrigin-RevId: 266790267 --- .../java/com/google/android/exoplayer2/util/AtomicFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 74e50dfd92..07a8b0a88a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -108,8 +108,9 @@ public final class AtomicFile { } catch (FileNotFoundException e) { File parent = baseName.getParentFile(); if (parent == null || !parent.mkdirs()) { - throw new IOException("Couldn't create directory " + baseName, e); + throw new IOException("Couldn't create " + baseName, e); } + // Try again now that we've created the parent directory. try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e2) { From c879bbf64cb8ce186491b431f03e90cce3a425d0 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 2 Sep 2019 18:05:18 +0100 Subject: [PATCH 247/424] move transparency of shuffle mode off button to bitmap PiperOrigin-RevId: 266795413 --- .../exoplayer2/ui/PlayerControlView.java | 13 ++++++--- .../exo_controls_shuffle_off.xml | 26 ++++++++++++++++++ ...huffle.xml => exo_controls_shuffle_on.xml} | 0 .../exo_controls_shuffle_off.png | Bin 0 -> 265 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 182 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 228 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 342 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 438 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin library/ui/src/main/res/values/styles.xml | 2 +- 14 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_shuffle.xml => exo_controls_shuffle_on.xml} (100%) create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-hdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-ldpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-mdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) 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 73bb98a1a0..e35169dd71 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 @@ -238,7 +238,7 @@ public class PlayerControlView extends FrameLayout { private final View fastForwardButton; private final View rewindButton; private final ImageView repeatToggleButton; - private final View shuffleButton; + private final ImageView shuffleButton; private final View vrButton; private final TextView durationView; private final TextView positionView; @@ -256,6 +256,8 @@ public class PlayerControlView extends FrameLayout { private final String repeatOffButtonContentDescription; private final String repeatOneButtonContentDescription; private final String repeatAllButtonContentDescription; + private final Drawable shuffleOnButtonDrawable; + private final Drawable shuffleOffButtonDrawable; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -407,6 +409,8 @@ public class PlayerControlView extends FrameLayout { repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_on); + shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_off); repeatOffButtonContentDescription = resources.getString(R.string.exo_controls_repeat_off_description); repeatOneButtonContentDescription = @@ -815,10 +819,11 @@ public class PlayerControlView extends FrameLayout { shuffleButton.setVisibility(GONE); } else if (player == null) { setButtonEnabled(false, shuffleButton); + shuffleButton.setImageDrawable(shuffleOffButtonDrawable); } else { - shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); - shuffleButton.setEnabled(true); - shuffleButton.setVisibility(VISIBLE); + setButtonEnabled(true, shuffleButton); + shuffleButton.setImageDrawable( + player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable); } } diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml new file mode 100644 index 0000000000..283ce30c3c --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..b693422db75eeef7c4beded7dbe9644eec85194a GIT binary patch literal 265 zcmV+k0rvihP)|OH=gIq{2%7koSSbot2;Bl*Z~rsziFwQQA0p}?-;bZ zA$d>OouIrYwyU7LBR<=|u)HG-;CVvud^j-CJO!?Lkvsv6n0USp%K gy{w(*k4Ut36#DLn;`P78pse{J9Po?zX{o-TQ{ZXM3MDneg;gita;L_f`?^_1 zl0MUbyBlW$9RnGj!Vh4CdW#`zCpT cnglDurOSd!uTxH$0iDg@>FVdQ&MBb@03C8#m;e9( literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..2b67cabf5afabec4433cb71905087d42cc094b89 GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7`%AFv@zmIEGX(zP-`Nb|^uj^TTd7ZCALw$Y0r&=p1Lj{YUe@tcqam3F*7V$q%g~zq$ zeqNj>wf&6!pX1j{=FJoT_W#WMlIPD^eY0-b{9hz|?fXTsbN|)Wd}85v(sM?NtyIO*{-%sBUL`7Ldk h!^?}awnnY}#`f#r3*~;M_+_B*@^tlcS?83{1OVHAnMMEr literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..22209d1f88712f3b615018fb1e7cfe572bdf256c GIT binary patch literal 438 zcmV;n0ZIOeP)A95OS2(`R-JKs2G;M#V*4;bHWx~}WGuIsw4 z>;9VVwAF*pWZGz)`kI&jrlF6^6K3X*1YvG|fG{;bK;Qtq`mz9l1N5vzkiY>E0ssUE zTI+xT0fO`Q8XOQJIG+Oo1m|-AC-8g@-~^tJ0f4~s_j=Nn0ssN$uaEEDng9TRfbx$s z0RRAj<(ptYfWY!iFd#ty`6eI$Agoxv2><{H8=h~1Pv;($!Pf3`RvnSWluE|5PJu+n=jp94&Y From c3f3b1bfa40674e6f7a6f5ced847ba569231a133 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 2 Sep 2019 18:28:20 +0100 Subject: [PATCH 248/424] Clarify LoadErrorHandlingPolicy's loadDurationMs javadocs PiperOrigin-RevId: 266797383 --- .../exoplayer2/upstream/LoadErrorHandlingPolicy.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java index 3432935d5f..293d1e7510 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -44,8 +44,8 @@ public interface LoadErrorHandlingPolicy { * * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to * load. - * @param loadDurationMs The duration in milliseconds of the load up to the point at which the - * error occurred, including any previous attempts. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. * @param exception The load error. * @param errorCount The number of errors this load has encountered, including this one. * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should @@ -64,8 +64,8 @@ public interface LoadErrorHandlingPolicy { * * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to * load. - * @param loadDurationMs The duration in milliseconds of the load up to the point at which the - * error occurred, including any previous attempts. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. * @param exception The load error. * @param errorCount The number of errors this load has encountered, including this one. * @return The number of milliseconds to wait before attempting the load again, or {@link From 332afc9f79bc9c2c7320f9e062ea095be32fce94 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Sep 2019 09:49:32 +0100 Subject: [PATCH 249/424] move transparency values of buttons to resources to make it accessible for customization PiperOrigin-RevId: 266880069 --- .../android/exoplayer2/ui/PlayerControlView.java | 11 ++++++++++- library/ui/src/main/res/values/constants.xml | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) 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 e35169dd71..e5b164ffb9 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 @@ -258,6 +258,8 @@ public class PlayerControlView extends FrameLayout { private final String repeatAllButtonContentDescription; private final Drawable shuffleOnButtonDrawable; private final Drawable shuffleOffButtonDrawable; + private final float buttonAlphaEnabled; + private final float buttonAlphaDisabled; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -405,7 +407,14 @@ public class PlayerControlView extends FrameLayout { } vrButton = findViewById(R.id.exo_vr); setShowVrButton(false); + Resources resources = context.getResources(); + + buttonAlphaEnabled = + (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100; + buttonAlphaDisabled = + (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100; + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); @@ -957,7 +966,7 @@ public class PlayerControlView extends FrameLayout { return; } view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.3f); + view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled); view.setVisibility(VISIBLE); } diff --git a/library/ui/src/main/res/values/constants.xml b/library/ui/src/main/res/values/constants.xml index 9b374d8382..9bd616583e 100644 --- a/library/ui/src/main/res/values/constants.xml +++ b/library/ui/src/main/res/values/constants.xml @@ -18,6 +18,9 @@ 71dp 52dp + 100 + 33 + #AA000000 #FFF4F3F0 From 5a516baa7860597ffe5df28f61af05e7bdd817aa Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 6 Sep 2019 07:54:35 +0100 Subject: [PATCH 250/424] Fix init data handling for FLAC in MP4 Issue: #6396 PiperOrigin-RevId: 267536336 --- RELEASENOTES.md | 10 ++++++---- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d3cbc6b2d7..602c823c89 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,12 +11,14 @@ quality video can be loaded up to the full default buffer duration. * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). -* Fix `PlayerNotificationManager` to show play icon rather than pause icon when - playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). -* Fix audio selection issue where languages are compared by bit rate - ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. +* Fix initialization data handling for FLAC in MP4 + ([#6396](https://github.com/google/ExoPlayer/issues/6396)). +* Fix audio selection issue where languages are compared by bit rate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon when + playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). * Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues ([#4200](https://github.com/google/ExoPlayer/issues/4200), [#4249](https://github.com/google/ExoPlayer/issues/4249), 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 70873825e3..0ffbdf0f80 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 @@ -1148,7 +1148,7 @@ import java.util.List; 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 || childAtomType == Atom.TYPE_alac) { + } else if (childAtomType == 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 72aa150f02e03b89c0b24c1983fd030873330208 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 6 Sep 2019 10:00:53 +0100 Subject: [PATCH 251/424] Handle potential timeline updates that switch from content to ad. We currently don't test if an ad needs to be played in case we are already playing content. This is to prevent recreating the current content period when an ad is marked as skipped. We prefer playing until the designated ad group position and appending another piece of content. This is less likely to cause visible discontinuities in case the ad group position is at a key frame boundary. However, this means we currently miss updates that require us to play an ad after a timeline update. PiperOrigin-RevId: 267553459 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 13 ++++++++++--- 1 file changed, 10 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 65a6866a9f..b6ac22059f 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 @@ -1324,9 +1324,16 @@ import java.util.concurrent.atomic.AtomicBoolean; timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); newContentPositionUs = defaultPosition.second; newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); - } else if (newPeriodId.isAd()) { - // Recheck if the current ad still needs to be played. - newPeriodId = queue.resolveMediaPeriodIdForAds(newPeriodId.periodUid, newContentPositionUs); + } else { + // Recheck if the current ad still needs to be played or if we need to start playing an ad. + newPeriodId = + queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs); + if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) { + // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and + // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential + // discontinuity until we reach the former next ad group position. + newPeriodId = playbackInfo.periodId; + } } if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { From b9ffea68314628e6c45e7d09a7ff4ff85e2a4c1f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 6 Sep 2019 11:15:33 +0100 Subject: [PATCH 252/424] Fix decoder selection for E-AC3 JOC streams Issue: #6398 PiperOrigin-RevId: 267563795 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 8 +++++--- .../android/exoplayer2/mediacodec/MediaCodecSelector.java | 3 ++- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 602c823c89..d50686f298 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ ([#6396](https://github.com/google/ExoPlayer/issues/6396)). * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Fix decoder selection for E-AC3 JOC streams + ([#6398](https://github.com/google/ExoPlayer/issues/6398)). * Fix `PlayerNotificationManager` to show play icon rather than pause icon when playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). * Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues 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 07a1438519..f10f45ecf3 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 @@ -45,6 +45,7 @@ import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -369,10 +370,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media 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 = + List decoderInfosWithEac3 = new ArrayList<>(decoderInfos); + decoderInfosWithEac3.addAll( mediaCodecSelector.getDecoderInfos( - MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); - decoderInfos.addAll(eac3DecoderInfos); + MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false)); + decoderInfos = decoderInfosWithEac3; } return Collections.unmodifiableList(decoderInfos); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index 41cb4ee04a..c6e93d104a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -51,7 +51,8 @@ public interface MediaCodecSelector { * @param mimeType The MIME type for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. * @param requiresTunnelingDecoder Whether a tunneling decoder is required. - * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. + * @return An unmodifiable list of {@link MediaCodecInfo}s corresponding to decoders. May be + * empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ List getDecoderInfos( 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 a6391e4cc7..671523b5e8 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 @@ -146,8 +146,8 @@ public final class MediaCodecUtil { * unless secure decryption really is required. * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless * tunneling really is required. - * @return A list of all {@link MediaCodecInfo}s for the given mime type, in the order given by - * {@link MediaCodecList}. + * @return An unmodifiable list of all {@link MediaCodecInfo}s for the given mime type, in the + * order given by {@link MediaCodecList}. * @throws DecoderQueryException If there was an error querying the available decoders. */ public static synchronized List getDecoderInfos( From 23ddfaa80a3564b7720dc2eab9b83109163c62d6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Sep 2019 17:42:15 +0100 Subject: [PATCH 253/424] Add fLaC prefix to FLAC initialization data The fLaC prefix is included in the initialization data output from the MKV extractor, so this is highly likely ot be the right thing to do. Issue: #6397 PiperOrigin-RevId: 268244365 --- RELEASENOTES.md | 3 ++- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d50686f298..e701a89d6c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,7 +14,8 @@ * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. * Fix initialization data handling for FLAC in MP4 - ([#6396](https://github.com/google/ExoPlayer/issues/6396)). + ([#6396](https://github.com/google/ExoPlayer/issues/6396), + [#6397](https://github.com/google/ExoPlayer/issues/6397)). * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Fix decoder selection for E-AC3 JOC streams 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 0ffbdf0f80..c4e6ef17c4 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 @@ -1148,7 +1148,16 @@ import java.util.List; System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); - } else if (childAtomType == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) { + } else if (childAtomType == Atom.TYPE_dfLa) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[4 + childAtomBodySize]; + initializationData[0] = 0x66; // f + initializationData[1] = 0x4C; // L + initializationData[2] = 0x61; // a + initializationData[3] = 0x43; // C + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_alac) { int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); From 4eda96dd666a837b402ce5add86ef17592e1a187 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 12 Sep 2019 14:41:47 +0100 Subject: [PATCH 254/424] disable seekbar in media style notification for live stream ISSUE: #6416 PiperOrigin-RevId: 268673895 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 7e72904078..280a5e3d4f 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 @@ -921,7 +921,9 @@ public final class MediaSessionConnector { } builder.putLong( MediaMetadataCompat.METADATA_KEY_DURATION, - player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration()); + player.isCurrentWindowDynamic() || player.getDuration() == C.TIME_UNSET + ? -1 + : player.getDuration()); long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId(); if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) { List queue = mediaController.getQueue(); From 480f73748dda46e586f0b47fc18a61f65dae1262 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Sep 2019 13:46:02 +0100 Subject: [PATCH 255/424] Upgrade to OkHttp 3.12.5 Issue: #4078 PiperOrigin-RevId: 268887744 --- RELEASENOTES.md | 4 +++- extensions/okhttp/build.gradle | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e701a89d6c..0339756923 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,7 +22,9 @@ ([#6398](https://github.com/google/ExoPlayer/issues/6398)). * Fix `PlayerNotificationManager` to show play icon rather than pause icon when playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). -* Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues +* OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues ([#4200](https://github.com/google/ExoPlayer/issues/4200), [#4249](https://github.com/google/ExoPlayer/issues/4249), [#4319](https://github.com/google/ExoPlayer/issues/4319), diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 68bd422185..2395aedd46 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -35,7 +35,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - api 'com.squareup.okhttp3:okhttp:3.12.1' + api 'com.squareup.okhttp3:okhttp:3.12.5' } ext { From 06a374e74b0e9f279c962e3586166b9be84c9044 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Sep 2019 17:25:53 -0700 Subject: [PATCH 256/424] Clean up release notes --- RELEASENOTES.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0339756923..602480d9f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,26 +2,20 @@ ### 2.10.5 ### -* Add `allowAudioMixedChannelCountAdaptiveness` parameter to - `DefaultTrackSelector` to allow adaptive selections of audio tracks with - different channel counts - ([#6257](https://github.com/google/ExoPlayer/issues/6257)). -* Reset `DefaultBandwidthMeter` to initial values on network change. -* Increase maximum buffer size for video in `DefaultLoadControl` to ensure high - quality video can be loaded up to the full default buffer duration. -* Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is - provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Track selection + * Fix audio selection issue where languages are compared by bitrate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). + * Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts. +* Performance + * Increase maximum video buffer size from 13MB to 32MB. The previous default + was too small for high quality streams. + * Reset `DefaultBandwidthMeter` to initial values on network change. + * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is + provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. -* Fix initialization data handling for FLAC in MP4 - ([#6396](https://github.com/google/ExoPlayer/issues/6396), - [#6397](https://github.com/google/ExoPlayer/issues/6397)). -* Fix audio selection issue where languages are compared by bit rate - ([#6335](https://github.com/google/ExoPlayer/issues/6335)). -* Fix decoder selection for E-AC3 JOC streams - ([#6398](https://github.com/google/ExoPlayer/issues/6398)). -* Fix `PlayerNotificationManager` to show play icon rather than pause icon when - playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). * OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue ([#4078](https://github.com/google/ExoPlayer/issues/4078)). * RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues @@ -29,6 +23,13 @@ [#4249](https://github.com/google/ExoPlayer/issues/4249), [#4319](https://github.com/google/ExoPlayer/issues/4319), [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* Fix initialization data handling for FLAC in MP4 + ([#6396](https://github.com/google/ExoPlayer/issues/6396), + [#6397](https://github.com/google/ExoPlayer/issues/6397)). +* Fix decoder selection for E-AC3 JOC streams + ([#6398](https://github.com/google/ExoPlayer/issues/6398)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon when + playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). ### 2.10.4 ### From 9bc44977499bf503ad261cd838f76446d4b8ada4 Mon Sep 17 00:00:00 2001 From: Toni Date: Wed, 24 Jul 2019 12:33:37 +0100 Subject: [PATCH 257/424] Merge pull request #6178 from xirac:feature/text-track-score PiperOrigin-RevId: 259707359 --- .../trackselection/DefaultTrackSelector.java | 225 +++++++++++------- .../DefaultTrackSelectorTest.java | 6 +- 2 files changed, 146 insertions(+), 85 deletions(-) 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 de4415ce39..5e5ad3b4bc 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 @@ -19,7 +19,6 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; @@ -1640,7 +1639,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - int selectedTextTrackScore = Integer.MIN_VALUE; + TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { int trackType = mappedTrackInfo.getRendererType(i); @@ -1650,13 +1649,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Already done. Do nothing. break; case C.TRACK_TYPE_TEXT: - Pair textSelection = + Pair textSelection = selectTextTrack( mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params, selectedAudioLanguage); - if (textSelection != null && textSelection.second > selectedTextTrackScore) { + if (textSelection != null + && (selectedTextTrackScore == null + || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { if (selectedTextRendererIndex != C.INDEX_UNSET) { // We've already made a selection for another text renderer, but it had a lower score. // Clear the selection for that renderer. @@ -2148,21 +2149,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { * track, indexed by track group index and track index (in that order). * @param params The selector's current constraint parameters. * @param selectedAudioLanguage The language of the selected audio track. May be null if the - * selected audio track declares no language or no audio track was selected. - * @return The {@link TrackSelection.Definition} and corresponding track score, or null if no - * selection was made. + * selected text track declares no language or no text track was selected. + * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null + * if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable - protected Pair selectTextTrack( + protected Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; - int selectedTrackScore = 0; + int selectedTrackIndex = C.INDEX_UNSET; + TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; @@ -2170,39 +2171,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - int maskedSelectionFlags = - format.selectionFlags & ~params.disabledTextTrackSelectionFlags; - boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; - int trackScore; - int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage); - boolean trackHasNoLanguage = formatHasNoLanguage(format); - if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) { - if (isDefault) { - 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 = 7; - } else { - trackScore = 3; - } - trackScore += languageScore; - } else if (isDefault) { - trackScore = 2; - } else if (isForced - && (getFormatLanguageScore(format, selectedAudioLanguage) > 0 - || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { - trackScore = 1; - } else { - // Track should not be selected. - continue; - } - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (trackScore > selectedTrackScore) { + TextTrackScore trackScore = + new TextTrackScore( + format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); + if (trackScore.isWithinConstraints + && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2213,7 +2186,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selectedGroup == null ? null : Pair.create( - new TrackSelection.Definition(selectedGroup, selectedTrackIndex), selectedTrackScore); + new TrackSelection.Definition(selectedGroup, selectedTrackIndex), + Assertions.checkNotNull(selectedTrackScore)); } // General track selection methods. @@ -2383,19 +2357,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - /** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */ - protected static boolean formatHasNoLanguage(Format format) { - return stringDefinesNoLanguage(format.language); - } - /** - * Returns whether the given string does not define a language. + * Normalizes the input string to null if it does not define a language, or returns it otherwise. * * @param language The string. - * @return Whether the given string does not define a language. + * @return The string, optionally normalized to null if it does not define a language. */ - protected static boolean stringDefinesNoLanguage(@Nullable String language) { - return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED); + @Nullable + protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) { + return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED) + ? null + : language; } /** @@ -2403,26 +2375,34 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param format The {@link Format}. * @param language The language, or null. - * @return A score of 3 if the languages match fully, a score of 2 if the languages match partly, - * a score of 1 if the languages don't match but belong to the same main language, and a score - * of 0 if the languages don't match at all. + * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format + * language tag are allowed. + * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly, + * a score of 2 if the languages don't match but belong to the same main language, a score of + * 1 if the format language is undetermined and such a match is allowed, and a score of 0 if + * the languages don't match at all. */ - protected static int getFormatLanguageScore(Format format, @Nullable String language) { - if (format.language == null || language == null) { - return 0; + protected static int getFormatLanguageScore( + Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) { + if (!TextUtils.isEmpty(language) && language.equals(format.language)) { + // Full literal match of non-empty languages, including matches of an explicit "und" query. + return 4; } - if (TextUtils.equals(format.language, language)) { + language = normalizeUndeterminedLanguageToNull(language); + String formatLanguage = normalizeUndeterminedLanguageToNull(format.language); + if (formatLanguage == null || language == null) { + // At least one of the languages is undetermined. + return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0; + } + if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) { + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") return 3; } - // 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. "fr-fr" and "fr-ca") - String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0]; + String formatMainLanguage = Util.splitAtFirst(formatLanguage, "-")[0]; String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; if (formatMainLanguage.equals(queryMainLanguage)) { - return 1; + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + return 2; } return 0; } @@ -2496,9 +2476,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + /** + * Compares two integers in a safe way avoiding potential overflow. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareInts(int first, int second) { + return first > second ? 1 : (second > first ? -1 : 0); + } + /** Represents how well an audio track matches the selection {@link Parameters}. */ protected static final class AudioTrackScore implements Comparable { + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ public final boolean isWithinConstraints; @Nullable private final String language; @@ -2516,7 +2512,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); - preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredAudioLanguage); + preferredLanguageScore = + getFormatLanguageScore( + format, + parameters.preferredAudioLanguage, + /* allowUndeterminedFormatLanguage= */ false); isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; channelCount = format.channelCount; sampleRate = format.sampleRate; @@ -2529,7 +2529,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { int bestMatchIndex = Integer.MAX_VALUE; int bestMatchScore = 0; for (int i = 0; i < localeLanguages.length; i++) { - int score = getFormatLanguageScore(format, localeLanguages[i]); + int score = + getFormatLanguageScore( + format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false); if (score > 0) { bestMatchIndex = i; bestMatchScore = score; @@ -2548,7 +2550,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * negative integer if this score is worse than the other. */ @Override - public int compareTo(@NonNull AudioTrackScore other) { + public int compareTo(AudioTrackScore other) { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } @@ -2590,18 +2592,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - /** - * Compares two integers in a safe way and avoiding potential overflow. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareInts(int first, int second) { - return first > second ? 1 : (second > first ? -1 : 0); - } - private static final class AudioConfigurationTuple { public final int channelCount; @@ -2637,4 +2627,75 @@ public class DefaultTrackSelector extends MappingTrackSelector { } + /** Represents how well a text track matches the selection {@link Parameters}. */ + protected static final class TextTrackScore implements Comparable { + + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ + public final boolean isWithinConstraints; + + private final boolean isWithinRendererCapabilities; + private final boolean isDefault; + private final boolean isForced; + private final int preferredLanguageScore; + private final boolean isForcedAndSelectedAudioLanguage; + + public TextTrackScore( + Format format, + Parameters parameters, + int trackFormatSupport, + @Nullable String selectedAudioLanguage) { + isWithinRendererCapabilities = + isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); + int maskedSelectionFlags = + format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + preferredLanguageScore = + getFormatLanguageScore( + format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + boolean selectedAudioLanguageUndetermined = + normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; + int selectedAudioLanguageScore = + getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); + isForcedAndSelectedAudioLanguage = isForced && selectedAudioLanguageScore > 0; + isWithinConstraints = + preferredLanguageScore > 0 || isDefault || isForcedAndSelectedAudioLanguage; + } + + /** + * Compares this score with another. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. + */ + @Override + public int compareTo(TextTrackScore other) { + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if ((this.preferredLanguageScore > 0) != (other.preferredLanguageScore > 0)) { + return this.preferredLanguageScore > 0 ? 1 : -1; + } + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.preferredLanguageScore > 0) { + if (this.isForced != other.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. + return !this.isForced ? 1 : -1; + } + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + } + if (this.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) { + return this.isForcedAndSelectedAudioLanguage ? 1 : -1; + } + return 0; + } + } } 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 92c4628fa0..c672972001 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 @@ -1048,12 +1048,12 @@ public final class DefaultTrackSelectorTest { result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); - // There is a preferred language, so the first language-matching track flagged as default should - // be selected. + // There is a preferred language, so a language-matching track flagged as default should + // be selected, and the one without forced flag should be preferred. trackSelector.setParameters( Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedDefault); + assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); // Same as above, but the default flag is disabled. If multiple tracks match the preferred // language, those not flagged as forced are preferred, as they likely include the contents of From 560c8c8760cd6f5b085e51030442bce8a4d75ea8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 11:05:27 +0100 Subject: [PATCH 258/424] Simplify and improve text selection logic. This changes the logic in the following ways: - If no preferred language is matched, prefer better scores for the selected audio language. - If a preferred language is matched, always prefer the better match irrespective of default or forced flags. - If a preferred language score and the isForced flag is the same, prefer tracks with a better selected audio language match. PiperOrigin-RevId: 259707430 --- RELEASENOTES.md | 2 ++ .../trackselection/DefaultTrackSelector.java | 35 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 602480d9f8..b490e3a314 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. +* Improve text selection logic to always prefer the better language matches + over other selection parameters. * OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue ([#4078](https://github.com/google/ExoPlayer/issues/4078)). * RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues 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 5e5ad3b4bc..0d35fcd65a 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 @@ -2638,9 +2638,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isWithinRendererCapabilities; private final boolean isDefault; - private final boolean isForced; + private final boolean hasPreferredIsForcedFlag; private final int preferredLanguageScore; - private final boolean isForcedAndSelectedAudioLanguage; + private final int selectedAudioLanguageScore; public TextTrackScore( Format format, @@ -2652,17 +2652,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; preferredLanguageScore = getFormatLanguageScore( format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + // Prefer non-forced to forced if a preferred text language has been matched. Where both are + // provided the non-forced track will usually contain the forced subtitles as a subset. + // Otherwise, prefer a forced track. + hasPreferredIsForcedFlag = + (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced); boolean selectedAudioLanguageUndetermined = normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; - int selectedAudioLanguageScore = + selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); - isForcedAndSelectedAudioLanguage = isForced && selectedAudioLanguageScore > 0; isWithinConstraints = - preferredLanguageScore > 0 || isDefault || isForcedAndSelectedAudioLanguage; + preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2677,25 +2681,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } - if ((this.preferredLanguageScore > 0) != (other.preferredLanguageScore > 0)) { - return this.preferredLanguageScore > 0 ? 1 : -1; + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); } if (this.isDefault != other.isDefault) { return this.isDefault ? 1 : -1; } - if (this.preferredLanguageScore > 0) { - if (this.isForced != other.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. - return !this.isForced ? 1 : -1; - } - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 1 : -1; } - if (this.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) { - return this.isForcedAndSelectedAudioLanguage ? 1 : -1; - } - return 0; + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); } } } From 26e293070e3f55fdb59331c6cf5caf1cadce3210 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 10 Sep 2019 17:43:21 +0100 Subject: [PATCH 259/424] Merge pull request #6158 from xirac:dev-v2 PiperOrigin-RevId: 268240722 --- .../trackselection/DefaultTrackSelector.java | 32 +++++++++++++++++-- .../TrackSelectionParameters.java | 26 +++++++++++++++ .../DefaultTrackSelectorTest.java | 1 + 3 files changed, 57 insertions(+), 2 deletions(-) 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 0d35fcd65a..b38710a67e 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 @@ -468,6 +468,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + @Override + public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { + super.setPreferredTextRoleFlags(preferredTextRoleFlags); + return this; + } + @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { @@ -701,6 +707,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, + preferredTextRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags, // General @@ -891,6 +898,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, + TrackSelectionParameters.DEFAULT.preferredTextRoleFlags, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags, // General @@ -924,6 +932,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, + @C.RoleFlags int preferredTextRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags, // General @@ -937,6 +946,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { super( preferredAudioLanguage, preferredTextLanguage, + preferredTextRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); // Video @@ -2640,7 +2650,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isDefault; private final boolean hasPreferredIsForcedFlag; private final int preferredLanguageScore; + private final int preferredRoleFlagsScore; private final int selectedAudioLanguageScore; + private final boolean hasCaptionRoleFlags; public TextTrackScore( Format format, @@ -2656,6 +2668,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { preferredLanguageScore = getFormatLanguageScore( format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + preferredRoleFlagsScore = + Integer.bitCount(format.roleFlags & parameters.preferredTextRoleFlags); + hasCaptionRoleFlags = + (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0; // Prefer non-forced to forced if a preferred text language has been matched. Where both are // provided the non-forced track will usually contain the forced subtitles as a subset. // Otherwise, prefer a forced track. @@ -2666,7 +2682,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); isWithinConstraints = - preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); + preferredLanguageScore > 0 + || (parameters.preferredTextLanguage == null && preferredRoleFlagsScore > 0) + || isDefault + || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2684,13 +2703,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.preferredLanguageScore != other.preferredLanguageScore) { return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); } + if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) { + return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore); + } if (this.isDefault != other.isDefault) { return this.isDefault ? 1 : -1; } if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { return this.hasPreferredIsForcedFlag ? 1 : -1; } - return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); + if (this.selectedAudioLanguageScore != other.selectedAudioLanguageScore) { + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); + } + if (preferredRoleFlagsScore == 0 && this.hasCaptionRoleFlags != other.hasCaptionRoleFlags) { + return this.hasCaptionRoleFlags ? -1 : 1; + } + return 0; } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index f10b2befaf..047791387e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -35,6 +35,7 @@ public class TrackSelectionParameters implements Parcelable { @Nullable /* package */ String preferredAudioLanguage; // Text @Nullable /* package */ String preferredTextLanguage; + @C.RoleFlags /* package */ int preferredTextRoleFlags; /* package */ boolean selectUndeterminedTextLanguage; @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; @@ -52,6 +53,7 @@ public class TrackSelectionParameters implements Parcelable { preferredAudioLanguage = initialValues.preferredAudioLanguage; // Text preferredTextLanguage = initialValues.preferredTextLanguage; + preferredTextRoleFlags = initialValues.preferredTextRoleFlags; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; } @@ -82,6 +84,17 @@ public class TrackSelectionParameters implements Parcelable { return this; } + /** + * Sets the preferred {@link C.RoleFlags} for text tracks. + * + * @param preferredTextRoleFlags Preferred text role flags. + * @return This builder. + */ + public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { + this.preferredTextRoleFlags = preferredTextRoleFlags; + return this; + } + /** * Sets whether a text track with undetermined language should be selected if no track with * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is @@ -116,6 +129,7 @@ public class TrackSelectionParameters implements Parcelable { preferredAudioLanguage, // Text preferredTextLanguage, + preferredTextRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); } @@ -136,6 +150,11 @@ public class TrackSelectionParameters implements Parcelable { * track if there is one, or no track otherwise. The default value is {@code null}. */ @Nullable public final String preferredTextLanguage; + /** + * The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there + * is one, or no track otherwise. The default value is {@code 0}. + */ + @C.RoleFlags public final int preferredTextRoleFlags; /** * Whether a text track with undetermined language should be selected if no track with {@link * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The @@ -153,6 +172,7 @@ public class TrackSelectionParameters implements Parcelable { /* preferredAudioLanguage= */ null, // Text /* preferredTextLanguage= */ null, + /* preferredTextRoleFlags= */ 0, /* selectUndeterminedTextLanguage= */ false, /* disabledTextTrackSelectionFlags= */ 0); } @@ -160,12 +180,14 @@ public class TrackSelectionParameters implements Parcelable { /* package */ TrackSelectionParameters( @Nullable String preferredAudioLanguage, @Nullable String preferredTextLanguage, + @C.RoleFlags int preferredTextRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags) { // Audio this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); // Text this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); + this.preferredTextRoleFlags = preferredTextRoleFlags; this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; } @@ -175,6 +197,7 @@ public class TrackSelectionParameters implements Parcelable { this.preferredAudioLanguage = in.readString(); // Text this.preferredTextLanguage = in.readString(); + this.preferredTextRoleFlags = in.readInt(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); this.disabledTextTrackSelectionFlags = in.readInt(); } @@ -197,6 +220,7 @@ public class TrackSelectionParameters implements Parcelable { return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) // Text && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) + && preferredTextRoleFlags == other.preferredTextRoleFlags && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; } @@ -208,6 +232,7 @@ public class TrackSelectionParameters implements Parcelable { result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); // Text result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); + result = 31 * result + preferredTextRoleFlags; result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); result = 31 * result + disabledTextTrackSelectionFlags; return result; @@ -226,6 +251,7 @@ public class TrackSelectionParameters implements Parcelable { dest.writeString(preferredAudioLanguage); // Text dest.writeString(preferredTextLanguage); + dest.writeInt(preferredTextRoleFlags); Util.writeBoolean(dest, selectUndeterminedTextLanguage); dest.writeInt(disabledTextTrackSelectionFlags); } 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 c672972001..3fad88dd9f 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 @@ -146,6 +146,7 @@ public final class DefaultTrackSelectorTest { /* allowAudioMixedChannelCountAdaptiveness= */ true, // Text /* preferredTextLanguage= */ "de", + /* preferredTextRoleFlags= */ C.ROLE_FLAG_CAPTION, /* selectUndeterminedTextLanguage= */ true, /* disabledTextTrackSelectionFlags= */ 8, // General From 1a4b1e1ea192d6702c536551b8e194473a3b4a54 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 23 Aug 2019 18:48:14 +0100 Subject: [PATCH 260/424] Revert "Add HTTP request parameters (headers) to DataSpec." This reverts commit c3d6be3afdd7c0ca68dba15e443bc64aa3f61073. --- .../android/exoplayer2/upstream/DataSpec.java | 60 +------- .../exoplayer2/upstream/DataSpecTest.java | 128 ------------------ 2 files changed, 6 insertions(+), 182 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 3563078c87..e32e063d0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -24,9 +24,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; /** * Defines a region of data. @@ -105,10 +102,9 @@ public final class DataSpec { /** @deprecated Use {@link #httpBody} instead. */ @Deprecated public final @Nullable byte[] postBody; - /** Immutable map containing the headers to use in HTTP requests. */ - public final Map httpRequestHeaders; - - /** The absolute position of the data in the full stream. */ + /** + * The absolute position of the data in the full stream. + */ public final long absoluteStreamPosition; /** * The position of the data when read from {@link #uri}. @@ -239,6 +235,7 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ + @SuppressWarnings("deprecation") public DataSpec( Uri uri, @HttpMethod int httpMethod, @@ -248,41 +245,6 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { - this( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - /* httpRequestHeaders= */ Collections.emptyMap()); - } - - /** - * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. - * - * @param uri {@link #uri}. - * @param httpMethod {@link #httpMethod}. - * @param httpBody {@link #httpBody}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}. - * @param position {@link #position}. - * @param length {@link #length}. - * @param key {@link #key}. - * @param flags {@link #flags}. - * @param httpRequestHeaders {@link #httpRequestHeaders}. - */ - public DataSpec( - Uri uri, - @HttpMethod int httpMethod, - @Nullable byte[] httpBody, - long absoluteStreamPosition, - long position, - long length, - @Nullable String key, - @Flags int flags, - Map httpRequestHeaders) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); @@ -295,7 +257,6 @@ public final class DataSpec { this.length = length; this.key = key; this.flags = flags; - this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -383,8 +344,7 @@ public final class DataSpec { position + offset, length, key, - flags, - httpRequestHeaders); + flags); } } @@ -396,14 +356,6 @@ public final class DataSpec { */ public DataSpec withUri(Uri uri) { return new DataSpec( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - httpRequestHeaders); + uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java deleted file mode 100644 index f6e30f814a..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 static com.google.common.truth.Truth.assertThat; -import static junit.framework.TestCase.fail; - -import android.net.Uri; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.util.HashMap; -import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link DataSpec}. */ -@RunWith(AndroidJUnit4.class) -public class DataSpecTest { - - @Test - public void createDataSpec_withDefaultValues_setsEmptyHttpRequestParameters() { - Uri uri = Uri.parse("www.google.com"); - DataSpec dataSpec = new DataSpec(uri); - - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - - dataSpec = new DataSpec(uri, /*flags= */ 0); - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - - dataSpec = - new DataSpec( - uri, - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0); - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - } - - @Test - public void createDataSpec_setsHttpRequestParameters() { - Map httpRequestParameters = new HashMap<>(); - httpRequestParameters.put("key1", "value1"); - httpRequestParameters.put("key2", "value2"); - httpRequestParameters.put("key3", "value3"); - - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - httpRequestParameters); - - assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestParameters); - } - - @Test - public void httpRequestParameters_areReadOnly() { - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - /* httpRequestHeaders= */ new HashMap<>()); - - try { - dataSpec.httpRequestHeaders.put("key", "value"); - fail(); - } catch (UnsupportedOperationException expected) { - // Expected - } - } - - @Test - public void copyMethods_copiesHttpRequestHeaders() { - Map httpRequestParameters = new HashMap<>(); - httpRequestParameters.put("key1", "value1"); - httpRequestParameters.put("key2", "value2"); - httpRequestParameters.put("key3", "value3"); - - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - httpRequestParameters); - - DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); - - dataSpecCopy = dataSpec.subrange(2); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); - - dataSpecCopy = dataSpec.subrange(2, 2); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); - } -} From 772b13999a6e976346f705fd63e725a919990097 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Sep 2019 18:09:09 -0700 Subject: [PATCH 261/424] Tweak release notes --- RELEASENOTES.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b490e3a314..0e2ab82101 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,21 +3,24 @@ ### 2.10.5 ### * Track selection - * Fix audio selection issue where languages are compared by bitrate - ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Add `allowAudioMixedChannelCountAdaptiveness` parameter to `DefaultTrackSelector` to allow adaptive selections of audio tracks with different channel counts. + * Improve text selection logic to always prefer the better language matches + over other selection parameters. + * Fix audio selection issue where languages are compared by bitrate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). * Performance * Increase maximum video buffer size from 13MB to 32MB. The previous default was too small for high quality streams. * Reset `DefaultBandwidthMeter` to initial values on network change. * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Metadata + * Support EMSG V1 boxes in FMP4. + * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. -* Improve text selection logic to always prefer the better language matches - over other selection parameters. * OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue ([#4078](https://github.com/google/ExoPlayer/issues/4078)). * RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues From 70731fe8b1ccffce0b79bdda4daf2a806770539a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Sep 2019 18:24:54 -0700 Subject: [PATCH 262/424] Further tweaking of release notes --- RELEASENOTES.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0e2ab82101..044174be07 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,13 +21,6 @@ * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). * Add `HttpDataSource.getResponseCode` to provide the status code associated with the most recent HTTP response. -* OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue - ([#4078](https://github.com/google/ExoPlayer/issues/4078)). -* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues - ([#4200](https://github.com/google/ExoPlayer/issues/4200), - [#4249](https://github.com/google/ExoPlayer/issues/4249), - [#4319](https://github.com/google/ExoPlayer/issues/4319), - [#4337](https://github.com/google/ExoPlayer/issues/4337)). * Fix initialization data handling for FLAC in MP4 ([#6396](https://github.com/google/ExoPlayer/issues/6396), [#6397](https://github.com/google/ExoPlayer/issues/6397)). @@ -35,6 +28,15 @@ ([#6398](https://github.com/google/ExoPlayer/issues/6398)). * Fix `PlayerNotificationManager` to show play icon rather than pause icon when playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). +* OkHttp extension: Upgrade OkHttp to fix HTTP2 socket timeout issue + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues + ([#4200](https://github.com/google/ExoPlayer/issues/4200), + [#4249](https://github.com/google/ExoPlayer/issues/4249), + [#4319](https://github.com/google/ExoPlayer/issues/4319), + [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged` + ([#5831](https://github.com/google/ExoPlayer/issues/5831)). ### 2.10.4 ### From b4a2f27cddb69a03370e2805a4abe59b720f759c Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 18:46:12 +0100 Subject: [PATCH 263/424] Expand FakeSampleStream to allow specifying a single sample I removed the buffer.flip() call because it seems incompatible with the way MetadataRenderer deals with the Stream - it calls flip() itself on line 126. Tests fail with flip() here, and pass without it... PiperOrigin-RevId: 263381799 --- .../exoplayer2/testutil/FakeSampleStream.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index a60c1c9c6d..ba604cb087 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -32,6 +32,7 @@ public final class FakeSampleStream implements SampleStream { private final Format format; private final @Nullable EventDispatcher eventDispatcher; + private final byte[] sampleData; private boolean notifiedDownstreamFormat; private boolean readFormat; @@ -47,9 +48,23 @@ public final class FakeSampleStream implements SampleStream { */ public FakeSampleStream( Format format, @Nullable EventDispatcher eventDispatcher, boolean shouldOutputSample) { + this(format, eventDispatcher, new byte[] {0}); + readSample = !shouldOutputSample; + } + + /** + * Creates fake sample stream which outputs the given {@link Format}, one sample with the provided + * bytes, then end of stream. + * + * @param format The {@link Format} to output. + * @param eventDispatcher An {@link EventDispatcher} to notify of read events. + * @param sampleData The sample data to output. + */ + public FakeSampleStream( + Format format, @Nullable EventDispatcher eventDispatcher, byte[] sampleData) { this.format = format; this.eventDispatcher = eventDispatcher; - readSample = !shouldOutputSample; + this.sampleData = sampleData; } @Override @@ -58,8 +73,8 @@ public final class FakeSampleStream implements SampleStream { } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { if (eventDispatcher != null && !notifiedDownstreamFormat) { eventDispatcher.downstreamFormatChanged( C.TRACK_TYPE_UNKNOWN, @@ -75,9 +90,8 @@ public final class FakeSampleStream implements SampleStream { return C.RESULT_FORMAT_READ; } else if (!readSample) { buffer.timeUs = 0; - buffer.ensureSpaceForWrite(1); - buffer.data.put((byte) 0); - buffer.flip(); + buffer.ensureSpaceForWrite(sampleData.length); + buffer.data.put(sampleData); readSample = true; return C.RESULT_BUFFER_READ; } else { @@ -95,5 +109,4 @@ public final class FakeSampleStream implements SampleStream { public int skipData(long positionUs) { return 0; } - } From 47e405ee113aa6a61b5d7f61c5034c6bcb6fa36e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Aug 2019 12:37:44 +0100 Subject: [PATCH 264/424] Add a metadata argument to Format factory methods used in HLS Required for propagation of HlsMetadataEntry's in chunkless preparation. PiperOrigin-RevId: 263324345 --- .../com/google/android/exoplayer2/Format.java | 16 ++++++++++++++-- .../source/dash/manifest/DashManifestParser.java | 2 ++ .../exoplayer2/source/dash/DashUtilTest.java | 1 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 ++ .../source/hls/playlist/HlsPlaylistParser.java | 3 +++ .../source/hls/HlsMediaPeriodTest.java | 3 +++ .../manifest/SsManifestParser.java | 2 ++ 7 files changed, 27 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index dcb7a83dca..d12c7ea18e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -168,6 +168,10 @@ public final class Format implements Parcelable { // Video. + /** + * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, float, List, int, int)} instead. + */ @Deprecated public static Format createVideoContainerFormat( @Nullable String id, @@ -186,6 +190,7 @@ public final class Format implements Parcelable { containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -201,6 +206,7 @@ public final class Format implements Parcelable { @Nullable String containerMimeType, String sampleMimeType, String codecs, + @Nullable Metadata metadata, int bitrate, int width, int height, @@ -215,7 +221,7 @@ public final class Format implements Parcelable { roleFlags, bitrate, codecs, - /* metadata= */ null, + metadata, containerMimeType, sampleMimeType, /* maxInputSize= */ NO_VALUE, @@ -345,6 +351,10 @@ public final class Format implements Parcelable { // Audio. + /** + * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, List, int, int, String)} instead. + */ @Deprecated public static Format createAudioContainerFormat( @Nullable String id, @@ -363,6 +373,7 @@ public final class Format implements Parcelable { containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, channelCount, sampleRate, @@ -378,6 +389,7 @@ public final class Format implements Parcelable { @Nullable String containerMimeType, @Nullable String sampleMimeType, @Nullable String codecs, + @Nullable Metadata metadata, int bitrate, int channelCount, int sampleRate, @@ -392,7 +404,7 @@ public final class Format implements Parcelable { roleFlags, bitrate, codecs, - /* metadata= */ null, + metadata, containerMimeType, sampleMimeType, /* maxInputSize= */ NO_VALUE, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index c3dfc3f136..0931396509 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -600,6 +600,7 @@ public class DashManifestParser extends DefaultHandler containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -614,6 +615,7 @@ public class DashManifestParser extends DefaultHandler containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, audioChannels, audioSamplingRate, diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index a53b1ff80d..6e769b72e1 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -80,6 +80,7 @@ public final class DashUtilTest { MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, /* codecs= */ "", + /* metadata= */ null, Format.NO_VALUE, /* width= */ 1024, /* height= */ 768, 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 2cfd14c79d..12e34019d6 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 @@ -773,6 +773,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -815,6 +816,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, channelCount, /* sampleRate= */ Format.NO_VALUE, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 42b27f259f..030520f8cb 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -349,6 +349,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser { MimeTypes.VIDEO_MP4, sampleMimeType, /* codecs= */ null, + /* metadata= */ null, bitrate, width, height, @@ -703,6 +704,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { MimeTypes.AUDIO_MP4, sampleMimeType, /* codecs= */ null, + /* metadata= */ null, bitrate, channels, samplingRate, From 66ba8d7793a18f50de31abf1887908030bda9c0d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Aug 2019 16:37:26 +0100 Subject: [PATCH 265/424] Fix propagation of HlsMetadataEntry's in HLS chunkless preparation PiperOrigin-RevId: 263356275 --- .../android/exoplayer2/source/hls/HlsMediaPeriod.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 12e34019d6..6381fff8dd 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -773,7 +774,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, - /* metadata= */ null, + variantFormat.metadata, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -786,6 +787,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private static Format deriveAudioFormat( Format variantFormat, Format mediaTagFormat, boolean isPrimaryTrackInVariant) { String codecs; + Metadata metadata; int channelCount = Format.NO_VALUE; int selectionFlags = 0; int roleFlags = 0; @@ -793,6 +795,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper String label = null; if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; + metadata = mediaTagFormat.metadata; channelCount = mediaTagFormat.channelCount; selectionFlags = mediaTagFormat.selectionFlags; roleFlags = mediaTagFormat.roleFlags; @@ -800,6 +803,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper label = mediaTagFormat.label; } else { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); + metadata = variantFormat.metadata; if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; @@ -816,7 +820,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, - /* metadata= */ null, + metadata, bitrate, channelCount, /* sampleRate= */ Format.NO_VALUE, From b2aa0ae0877f15581247c7435fee4ec846287e0e Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Sep 2019 10:17:27 +0100 Subject: [PATCH 266/424] provide content description for shuffle on/off button PiperOrigin-RevId: 266884166 --- .../android/exoplayer2/ui/PlayerControlView.java | 12 ++++++++++++ .../res/layout/exo_playback_control_view.xml | 2 +- library/ui/src/main/res/values-af/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-am/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ar/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-az/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-b+sr+Latn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-be/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bg/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bs/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ca/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-cs/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-da/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-de/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-el/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rAU/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rGB/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rIN/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-es-rUS/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-es/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-et/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-eu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fa/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fi/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-fr-rCA/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-gl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-gu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hi/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hy/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-in/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-is/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-it/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-iw/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ja/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ka/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-kk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-km/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-kn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ko/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ky/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lo/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lt/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lv/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ml/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ms/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-my/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-nb/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ne/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-nl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pa/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pl/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-pt-rPT/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pt/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ro/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ru/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-si/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sq/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sv/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sw/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ta/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-te/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-th/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-tl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-tr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-uk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ur/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-uz/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-vi/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rCN/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rHK/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rTW/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-zu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values/strings.xml | 6 ++++-- library/ui/src/main/res/values/styles.xml | 5 ----- 84 files changed, 1217 insertions(+), 88 deletions(-) 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 e5b164ffb9..358dd14576 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 @@ -260,6 +260,8 @@ public class PlayerControlView extends FrameLayout { private final Drawable shuffleOffButtonDrawable; private final float buttonAlphaEnabled; private final float buttonAlphaDisabled; + private final String shuffleOnContentDescription; + private final String shuffleOffContentDescription; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -426,6 +428,9 @@ public class PlayerControlView extends FrameLayout { resources.getString(R.string.exo_controls_repeat_one_description); repeatAllButtonContentDescription = resources.getString(R.string.exo_controls_repeat_all_description); + shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description); + shuffleOffContentDescription = + resources.getString(R.string.exo_controls_shuffle_off_description); } @SuppressWarnings("ResourceType") @@ -798,6 +803,8 @@ public class PlayerControlView extends FrameLayout { } if (player == null) { setButtonEnabled(false, repeatToggleButton); + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); return; } setButtonEnabled(true, repeatToggleButton); @@ -829,10 +836,15 @@ public class PlayerControlView extends FrameLayout { } else if (player == null) { setButtonEnabled(false, shuffleButton); shuffleButton.setImageDrawable(shuffleOffButtonDrawable); + shuffleButton.setContentDescription(shuffleOffContentDescription); } else { setButtonEnabled(true, shuffleButton); shuffleButton.setImageDrawable( player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable); + shuffleButton.setContentDescription( + player.getShuffleModeEnabled() + ? shuffleOnContentDescription + : shuffleOffContentDescription); } } 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 027e57ee92..acfddf1146 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 @@ -37,7 +37,7 @@ style="@style/ExoMediaButton.Rewind"/> + style="@style/ExoMediaButton"/> diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 8a983c543a..fa630292a9 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -1,4 +1,18 @@ + Vorige snit Volgende snit @@ -10,7 +24,7 @@ Herhaal niks Herhaal een Herhaal alles - Skommel + Skommel Volskermmodus VR-modus Aflaai diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index f56a6c06bf..b754aa90ea 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -1,4 +1,18 @@ + ቀዳሚ ትራክ ቀጣይ ትራክ @@ -10,7 +24,7 @@ ምንም አትድገም አንድ ድገም ሁሉንም ድገም - በውዝ + በውዝ የሙሉ ማያ ሁነታ የቪአር ሁነታ አውርድ diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 91063e1a54..87cad1be25 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -1,4 +1,18 @@ + المقطع الصوتي السابق المقطع الصوتي التالي @@ -10,7 +24,7 @@ عدم التكرار تكرار مقطع صوتي واحد تكرار الكل - ترتيب عشوائي + ترتيب عشوائي وضع ملء الشاشة وضع VR تنزيل diff --git a/library/ui/src/main/res/values-az/strings.xml b/library/ui/src/main/res/values-az/strings.xml index 0f5fbe3f4d..ddfc653731 100644 --- a/library/ui/src/main/res/values-az/strings.xml +++ b/library/ui/src/main/res/values-az/strings.xml @@ -1,4 +1,18 @@ + Əvvəlki trek Növbəti trek @@ -10,7 +24,7 @@ Heç biri təkrarlanmasın Biri təkrarlansın Hamısı təkrarlansın - Qarışdırın + Qarışdırın Tam ekran rejimi VR rejimi Endirin diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index 16300747f7..73c4223d8c 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -1,4 +1,18 @@ + Prethodna pesma Sledeća pesma @@ -10,7 +24,7 @@ Ne ponavljaj nijednu Ponovi jednu Ponovi sve - Pusti nasumično + Pusti nasumično Režim celog ekrana VR režim Preuzmi diff --git a/library/ui/src/main/res/values-be/strings.xml b/library/ui/src/main/res/values-be/strings.xml index 6a33be2a8f..7187494eca 100644 --- a/library/ui/src/main/res/values-be/strings.xml +++ b/library/ui/src/main/res/values-be/strings.xml @@ -1,4 +1,18 @@ + Папярэдні трэк Наступны трэк @@ -10,7 +24,7 @@ Не паўтараць нічога Паўтарыць адзін элемент Паўтарыць усе - Перамяшаць + Перамяшаць Поўнаэкранны рэжым VR-рэжым Спампаваць diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index 511a5e4f19..f7dcd29e49 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -1,4 +1,18 @@ + Предишен запис Следващ запис @@ -10,7 +24,7 @@ Без повтаряне Повтаряне на един елемент Повтаряне на всички - Разбъркване + Разбъркване Режим на цял екран режим за VR Изтегляне diff --git a/library/ui/src/main/res/values-bn/strings.xml b/library/ui/src/main/res/values-bn/strings.xml index cca445feca..6ccd22744c 100644 --- a/library/ui/src/main/res/values-bn/strings.xml +++ b/library/ui/src/main/res/values-bn/strings.xml @@ -1,4 +1,18 @@ + আগের ট্র্যাক পরবর্তী ট্র্যাক @@ -10,7 +24,7 @@ কোনও আইটেম আবার চালাবেন না একটি আইটেম আবার চালান সবগুলি আইটেম আবার চালান - শাফেল করুন + শাফেল করুন পূর্ণ স্ক্রিন মোড ভিআর মোড ডাউনলোড করুন diff --git a/library/ui/src/main/res/values-bs/strings.xml b/library/ui/src/main/res/values-bs/strings.xml index 24fb7b2b3b..a9a960285f 100644 --- a/library/ui/src/main/res/values-bs/strings.xml +++ b/library/ui/src/main/res/values-bs/strings.xml @@ -1,4 +1,18 @@ + Prethodna numera Sljedeća numera @@ -10,7 +24,7 @@ Ne ponavljaj Ponovi jedno Ponovi sve - Izmiješaj + Izmiješaj Način rada preko cijelog ekrana VR način rada Preuzmi diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index 3b48eab3b8..39a3ce85c9 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista següent @@ -10,7 +24,7 @@ No en repeteixis cap Repeteix una Repeteix tot - Reprodueix aleatòriament + Reprodueix aleatòriament Mode de pantalla completa Mode RV Baixa diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 1568074f9f..1ad837b32d 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -1,4 +1,18 @@ + Předchozí skladba Další skladba @@ -10,7 +24,7 @@ Neopakovat Opakovat jednu Opakovat vše - Náhodně + Náhodně Režim celé obrazovky Režim VR Stáhnout diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 19b0f09446..2bef98e781 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -1,4 +1,18 @@ + Afspil forrige Afspil næste @@ -10,7 +24,7 @@ Gentag ingen Gentag én Gentag alle - Bland + Bland Fuld skærm VR-tilstand Download diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index 1bb620dd2b..e06459de8d 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -1,4 +1,18 @@ + Vorheriger Titel Nächster Titel @@ -10,7 +24,7 @@ Keinen wiederholen Einen wiederholen Alle wiederholen - Zufallsmix + Zufallsmix Vollbildmodus VR-Modus Herunterladen diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 1ddbe4a5fa..47144dc00c 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -1,4 +1,18 @@ + Προηγούμενο κομμάτι Επόμενο κομμάτι @@ -10,7 +24,7 @@ Καμία επανάληψη Επανάληψη ενός κομματιού Επανάληψη όλων - Τυχαία αναπαραγωγή + Τυχαία αναπαραγωγή Λειτουργία πλήρους οθόνης Λειτουργία VR mode Λήψη diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index ceeb0b8497..beeeba4e9c 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista siguiente @@ -10,7 +24,7 @@ No repetir Repetir uno Repetir todo - Reproducir aleatoriamente + Reproducir aleatoriamente Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 0118da57be..e880d66bf0 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Siguiente pista @@ -10,7 +24,7 @@ No repetir Repetir uno Repetir todo - Reproducir aleatoriamente + Reproducir aleatoriamente Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-et/strings.xml b/library/ui/src/main/res/values-et/strings.xml index 99ca9548ed..515c665181 100644 --- a/library/ui/src/main/res/values-et/strings.xml +++ b/library/ui/src/main/res/values-et/strings.xml @@ -1,4 +1,18 @@ + Eelmine lugu Järgmine lugu @@ -10,7 +24,7 @@ Ära korda ühtegi Korda ühte Korda kõiki - Esita juhuslikus järjekorras + Esita juhuslikus järjekorras Täisekraani režiim VR-režiim Allalaadimine diff --git a/library/ui/src/main/res/values-eu/strings.xml b/library/ui/src/main/res/values-eu/strings.xml index 4d992fee0f..3f3d75d4f8 100644 --- a/library/ui/src/main/res/values-eu/strings.xml +++ b/library/ui/src/main/res/values-eu/strings.xml @@ -1,4 +1,18 @@ + Aurreko pista Hurrengo pista @@ -10,7 +24,7 @@ Ez errepikatu Errepikatu bat Errepikatu guztiak - Erreproduzitu ausaz + Erreproduzitu ausaz Pantaila osoko modua EB modua Deskargak diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index fed94b5569..dfc74a9c21 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -1,4 +1,18 @@ + آهنگ قبلی آهنگ بعدی @@ -10,7 +24,7 @@ تکرار هیچ‌کدام یکبار تکرار تکرار همه - درهم + درهم حالت تمام‌صفحه حالت واقعیت مجازی بارگیری diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 0dc2f9d346..c0e53c437b 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -1,4 +1,18 @@ + Edellinen kappale Seuraava kappale @@ -10,7 +24,7 @@ Ei uudelleentoistoa Toista yksi uudelleen Toista kaikki uudelleen - Satunnaistoisto + Satunnaistoisto Koko näytön tila VR-tila Lataa diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index 0f3534924f..ef42066df3 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -1,4 +1,18 @@ + Chanson précédente Chanson suivante @@ -10,7 +24,7 @@ Ne rien lire en boucle Lire une chanson en boucle Tout lire en boucle - Lecture aléatoire + Lecture aléatoire Mode Plein écran Mode RV Télécharger diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index 46c07f531e..057a6a8f67 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -1,4 +1,18 @@ + Titre précédent Titre suivant @@ -10,7 +24,7 @@ Ne rien lire en boucle Lire un titre en boucle Tout lire en boucle - Aléatoire + Aléatoire Mode plein écran Mode RV Télécharger diff --git a/library/ui/src/main/res/values-gl/strings.xml b/library/ui/src/main/res/values-gl/strings.xml index e6689353f1..419ea0c552 100644 --- a/library/ui/src/main/res/values-gl/strings.xml +++ b/library/ui/src/main/res/values-gl/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista seguinte @@ -10,7 +24,7 @@ Non repetir Repetir unha pista Repetir todas as pistas - Reprodución aleatoria + Reprodución aleatoria Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-gu/strings.xml b/library/ui/src/main/res/values-gu/strings.xml index 488eb39f6a..daec2b447d 100644 --- a/library/ui/src/main/res/values-gu/strings.xml +++ b/library/ui/src/main/res/values-gu/strings.xml @@ -1,4 +1,18 @@ + પહેલાંનો ટ્રૅક આગલો ટ્રૅક @@ -10,7 +24,7 @@ કોઈ રિપીટ કરતા નહીં એક રિપીટ કરો બધાને રિપીટ કરો - શફલ કરો + શફલ કરો પૂર્ણસ્ક્રીન મોડ VR મોડ ડાઉનલોડ કરો diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 8ba92054ff..0435e3eb84 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -1,4 +1,18 @@ + पिछला ट्रैक अगला ट्रैक @@ -10,7 +24,7 @@ किसी को न दोहराएं एक को दोहराएं सभी को दोहराएं - शफ़ल करें + शफ़ल करें फ़ुलस्क्रीन मोड VR मोड डाउनलोड करें diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index 4fa1942986..b36c9ff9e7 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -1,4 +1,18 @@ + Prethodni zapis Sljedeći zapis @@ -10,7 +24,7 @@ Bez ponavljanja Ponovi jedno Ponovi sve - Reproduciraj nasumično + Reproduciraj nasumično Prikaz na cijelom zaslonu VR način Preuzmi diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index baf77650e0..ad67165cf8 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -1,4 +1,18 @@ + Előző szám Következő szám @@ -10,7 +24,7 @@ Nincs ismétlés Egy szám ismétlése Összes szám ismétlése - Keverés + Keverés Teljes képernyős mód VR-mód Letöltés diff --git a/library/ui/src/main/res/values-hy/strings.xml b/library/ui/src/main/res/values-hy/strings.xml index 04a2aeb140..31f4db37d2 100644 --- a/library/ui/src/main/res/values-hy/strings.xml +++ b/library/ui/src/main/res/values-hy/strings.xml @@ -1,4 +1,18 @@ + Նախորդը Հաջորդը @@ -10,7 +24,7 @@ Չկրկնել Կրկնել մեկը Կրկնել բոլորը - Խառնել + Խառնել Լիաէկրան ռեժիմ VR ռեժիմ Ներբեռնել diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 7410576e81..d7bae9719d 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -1,4 +1,18 @@ + Lagu sebelumnya Lagu berikutnya @@ -10,7 +24,7 @@ Jangan ulangi Ulangi 1 Ulangi semua - Acak + Acak Mode layar penuh Mode VR Download diff --git a/library/ui/src/main/res/values-is/strings.xml b/library/ui/src/main/res/values-is/strings.xml index bdb27a6648..4c09db5251 100644 --- a/library/ui/src/main/res/values-is/strings.xml +++ b/library/ui/src/main/res/values-is/strings.xml @@ -1,4 +1,18 @@ + Fyrra lag Næsta lag @@ -10,7 +24,7 @@ Endurtaka ekkert Endurtaka eitt Endurtaka allt - Stokka + Stokka Allur skjárinn sýndarveruleikastilling Sækja diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index ffa05821e9..e10a62a11b 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -1,4 +1,18 @@ + Traccia precedente Traccia successiva @@ -10,7 +24,7 @@ Non ripetere nulla Ripeti uno Ripeti tutto - Riproduzione casuale + Riproduzione casuale Modalità a schermo intero Modalità VR Scarica diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index 695196c5be..8dd08278a3 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -1,4 +1,18 @@ + הרצועה הקודמת הרצועה הבאה @@ -10,7 +24,7 @@ אל תחזור על אף פריט חזור על פריט אחד חזור על הכול - ערבוב + ערבוב מצב מסך מלא מצב VR הורדה diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index b4158736a8..dc479596b9 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -1,4 +1,18 @@ + 前のトラック 次のトラック @@ -10,7 +24,7 @@ リピートなし 1 曲をリピート 全曲をリピート - シャッフル + シャッフル 全画面モード VR モード ダウンロード diff --git a/library/ui/src/main/res/values-ka/strings.xml b/library/ui/src/main/res/values-ka/strings.xml index 13ceaaf51f..7b9ecc7a3a 100644 --- a/library/ui/src/main/res/values-ka/strings.xml +++ b/library/ui/src/main/res/values-ka/strings.xml @@ -1,4 +1,18 @@ + წინა ჩანაწერი შემდეგი ჩანაწერი @@ -10,7 +24,7 @@ არცერთის გამეორება ერთის გამეორება ყველას გამეორება - არეულად დაკვრა + არეულად დაკვრა სრულეკრანიანი რეჟიმი VR რეჟიმი ჩამოტვირთვა diff --git a/library/ui/src/main/res/values-kk/strings.xml b/library/ui/src/main/res/values-kk/strings.xml index 92119d1fe5..ef2c1ab2b7 100644 --- a/library/ui/src/main/res/values-kk/strings.xml +++ b/library/ui/src/main/res/values-kk/strings.xml @@ -1,4 +1,18 @@ + Алдыңғы аудиотрек Келесі аудиотрек @@ -10,7 +24,7 @@ Ешқайсысын қайталамау Біреуін қайталау Барлығын қайталау - Араластыру + Араластыру Толық экран режимі VR режимі Жүктеп алу diff --git a/library/ui/src/main/res/values-km/strings.xml b/library/ui/src/main/res/values-km/strings.xml index 62728de026..3636a6e6d6 100644 --- a/library/ui/src/main/res/values-km/strings.xml +++ b/library/ui/src/main/res/values-km/strings.xml @@ -1,4 +1,18 @@ + សំនៀង​​មុន សំនៀង​បន្ទាប់ @@ -10,7 +24,7 @@ មិន​លេង​ឡើងវិញ លេង​ឡើង​វិញ​ម្ដង លេង​ឡើងវិញ​ទាំងអស់ - ច្របល់ + ច្របល់ មុខងារពេញ​អេក្រង់ មុខងារ VR ទាញយក diff --git a/library/ui/src/main/res/values-kn/strings.xml b/library/ui/src/main/res/values-kn/strings.xml index 6e6bfcb165..85df144fca 100644 --- a/library/ui/src/main/res/values-kn/strings.xml +++ b/library/ui/src/main/res/values-kn/strings.xml @@ -1,4 +1,18 @@ + ಹಿಂದಿನ ಟ್ರ್ಯಾಕ್ ಮುಂದಿನ ಟ್ರ್ಯಾಕ್ @@ -10,7 +24,7 @@ ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ - ಶಫಲ್‌ + ಶಫಲ್‌ ಪೂರ್ಣ ಪರದೆ ಮೋಡ್ VR ಮೋಡ್ ಡೌನ್‌ಲೋಡ್‌ diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 230660ad6a..3442318047 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -1,4 +1,18 @@ + 이전 트랙 다음 트랙 @@ -10,7 +24,7 @@ 반복 안함 현재 미디어 반복 모두 반복 - 셔플 + 셔플 전체화면 모드 가상 현실 모드 다운로드 diff --git a/library/ui/src/main/res/values-ky/strings.xml b/library/ui/src/main/res/values-ky/strings.xml index 57b8bbb5f5..a4c5b36a6c 100644 --- a/library/ui/src/main/res/values-ky/strings.xml +++ b/library/ui/src/main/res/values-ky/strings.xml @@ -1,4 +1,18 @@ + Мурунку трек Кийинки трек @@ -10,7 +24,7 @@ Кайталанбасын Бирөөнү кайталоо Баарын кайталоо - Аралаштыруу + Аралаштыруу Толук экран режими VR режими Жүктөп алуу diff --git a/library/ui/src/main/res/values-lo/strings.xml b/library/ui/src/main/res/values-lo/strings.xml index d7996610b2..8d380f2808 100644 --- a/library/ui/src/main/res/values-lo/strings.xml +++ b/library/ui/src/main/res/values-lo/strings.xml @@ -1,4 +1,18 @@ + ເພງກ່ອນໜ້າ ເພງຕໍ່ໄປ @@ -10,7 +24,7 @@ ບໍ່ຫຼິ້ນຊ້ຳ ຫຼິ້ນຊໍ້າ ຫຼິ້ນຊ້ຳທັງໝົດ - ຫຼີ້ນແບບສຸ່ມ + ຫຼີ້ນແບບສຸ່ມ ໂໝດເຕັມຈໍ ໂໝດ VR ດາວໂຫລດ diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 3e9a63dc99..1b3cfe4573 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -1,4 +1,18 @@ + Ankstesnis takelis Kitas takelis @@ -10,7 +24,7 @@ Nekartoti nieko Kartoti vieną Kartoti viską - Maišyti + Maišyti Viso ekrano režimas VR režimas Atsisiųsti diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index 59b541808a..6d7a232bcc 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -1,4 +1,18 @@ + Iepriekšējais ieraksts Nākamais ieraksts @@ -10,7 +24,7 @@ Neatkārtot nevienu Atkārtot vienu Atkārtot visu - Atskaņot jauktā secībā + Atskaņot jauktā secībā Pilnekrāna režīms VR režīms Lejupielādēt diff --git a/library/ui/src/main/res/values-mk/strings.xml b/library/ui/src/main/res/values-mk/strings.xml index 08a54d7240..1ad12a14d7 100644 --- a/library/ui/src/main/res/values-mk/strings.xml +++ b/library/ui/src/main/res/values-mk/strings.xml @@ -1,4 +1,18 @@ + Претходна песна Следна песна @@ -10,7 +24,7 @@ Не повторувај ниту една Повтори една Повтори ги сите - Измешај + Измешај Режим на цел екран Режим на VR Преземи diff --git a/library/ui/src/main/res/values-ml/strings.xml b/library/ui/src/main/res/values-ml/strings.xml index 6e79db0903..a227434e7a 100644 --- a/library/ui/src/main/res/values-ml/strings.xml +++ b/library/ui/src/main/res/values-ml/strings.xml @@ -1,4 +1,18 @@ + മുമ്പത്തെ ട്രാക്ക് അടുത്ത ട്രാക്ക് @@ -10,7 +24,7 @@ ഒന്നും ആവർത്തിക്കരുത് ഒരെണ്ണം ആവർത്തിക്കുക എല്ലാം ആവർത്തിക്കുക - ഇടകലര്‍ത്തുക + ഇടകലര്‍ത്തുക പൂർണ്ണ സ്‌ക്രീൻ മോഡ് VR മോഡ് ഡൗൺലോഡ് diff --git a/library/ui/src/main/res/values-mn/strings.xml b/library/ui/src/main/res/values-mn/strings.xml index 383d102520..8b8df3f9d4 100644 --- a/library/ui/src/main/res/values-mn/strings.xml +++ b/library/ui/src/main/res/values-mn/strings.xml @@ -1,4 +1,18 @@ + Өмнөх бичлэг Дараагийн бичлэг @@ -10,7 +24,7 @@ Алийг нь ч дахин тоглуулахгүй Одоогийн тоглуулж буй медиаг дахин тоглуулах Бүгдийг нь дахин тоглуулах - Холих + Холих Бүтэн дэлгэцийн горим VR горим Татах diff --git a/library/ui/src/main/res/values-mr/strings.xml b/library/ui/src/main/res/values-mr/strings.xml index a0900ab851..5c2bbc738c 100644 --- a/library/ui/src/main/res/values-mr/strings.xml +++ b/library/ui/src/main/res/values-mr/strings.xml @@ -1,4 +1,18 @@ + मागील ट्रॅक पुढील ट्रॅक @@ -10,7 +24,7 @@ रीपीट करू नका एक रीपीट करा सर्व रीपीट करा - शफल करा + शफल करा फुल स्क्रीन मोड VR मोड डाउनलोड करा diff --git a/library/ui/src/main/res/values-ms/strings.xml b/library/ui/src/main/res/values-ms/strings.xml index 6dab5be8de..8bc50c7605 100644 --- a/library/ui/src/main/res/values-ms/strings.xml +++ b/library/ui/src/main/res/values-ms/strings.xml @@ -1,4 +1,18 @@ + Lagu sebelumnya Lagu seterusnya @@ -10,7 +24,7 @@ Jangan ulang Ulang satu Ulang semua - Rombak + Rombak Mod skrin penuh Mod VR Muat turun diff --git a/library/ui/src/main/res/values-my/strings.xml b/library/ui/src/main/res/values-my/strings.xml index b30b76d516..e8a88a312d 100644 --- a/library/ui/src/main/res/values-my/strings.xml +++ b/library/ui/src/main/res/values-my/strings.xml @@ -1,4 +1,18 @@ + ယခင် တစ်ပုဒ် နောက် တစ်ပုဒ် @@ -10,7 +24,7 @@ မည်သည်ကိုမျှ ပြန်မကျော့ရန် တစ်ခုကို ပြန်ကျော့ရန် အားလုံး ပြန်ကျော့ရန် - ရောသမမွှေ + ရောသမမွှေ မျက်နှာပြင်အပြည့် မုဒ် VR မုဒ် ဒေါင်းလုဒ် လုပ်ရန် diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index f2847dd829..f9a0850bec 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -1,4 +1,18 @@ + Forrige spor Neste spor @@ -10,7 +24,7 @@ Ikke gjenta noen Gjenta én Gjenta alle - Tilfeldig rekkefølge + Tilfeldig rekkefølge Fullskjermmodus VR-modus Last ned diff --git a/library/ui/src/main/res/values-ne/strings.xml b/library/ui/src/main/res/values-ne/strings.xml index ff56480df1..f633a13af4 100644 --- a/library/ui/src/main/res/values-ne/strings.xml +++ b/library/ui/src/main/res/values-ne/strings.xml @@ -1,4 +1,18 @@ + अघिल्लो ट्रयाक अर्को ट्र्याक @@ -10,7 +24,7 @@ कुनै पनि नदोहोर्‍याउनुहोस् एउटा दोहोर्‍याउनुहोस् सबै दोहोर्‍याउनुहोस् - मिसाउनुहोस् + मिसाउनुहोस् पूर्ण स्क्रिन मोड VR मोड डाउनलोड गर्नुहोस् diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 3fbf113f1e..4c71815136 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -1,4 +1,18 @@ + Vorige track Volgende track @@ -10,7 +24,7 @@ Niets herhalen Eén herhalen Alles herhalen - Shuffle + Shuffle Modus \'Volledig scherm\' VR-modus Downloaden diff --git a/library/ui/src/main/res/values-pa/strings.xml b/library/ui/src/main/res/values-pa/strings.xml index 9f25759878..0d30c2c519 100644 --- a/library/ui/src/main/res/values-pa/strings.xml +++ b/library/ui/src/main/res/values-pa/strings.xml @@ -1,4 +1,18 @@ + ਪਿਛਲਾ ਟਰੈਕ ਅਗਲਾ ਟਰੈਕ @@ -10,7 +24,7 @@ ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ - ਬੇਤਰਤੀਬ ਕਰੋ + ਬੇਤਰਤੀਬ ਕਰੋ ਪੂਰੀ-ਸਕ੍ਰੀਨ ਮੋਡ VR ਮੋਡ ਡਾਊਨਲੋਡ ਕਰੋ diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 8df3b62b0c..46f76e975a 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -1,4 +1,18 @@ + Poprzedni utwór Następny utwór @@ -10,7 +24,7 @@ Nie powtarzaj Powtórz jeden Powtórz wszystkie - Odtwarzanie losowe + Odtwarzanie losowe Tryb pełnoekranowy Tryb VR Pobierz diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index 188e18f6b5..60df32be81 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -1,4 +1,18 @@ + Faixa anterior Faixa seguinte @@ -10,7 +24,7 @@ Não repetir nenhum Repetir um Repetir tudo - Reproduzir aleatoriamente + Reproduzir aleatoriamente Modo de ecrã inteiro Modo de RV Transferir diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 9e83387a76..63f3abd343 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -1,4 +1,18 @@ + Faixa anterior Próxima faixa @@ -10,7 +24,7 @@ Não repetir Repetir uma Repetir tudo - Aleatório + Aleatório Modo de tela cheia Modo RV Fazer o download diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 9bb8cfc8ee..b7f5a6b63e 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -1,4 +1,18 @@ + Melodia anterioară Următoarea înregistrare @@ -10,7 +24,7 @@ Nu repetați niciunul Repetați unul Repetați-le pe toate - Redați aleatoriu + Redați aleatoriu Modul Ecran complet Mod RV Descărcați diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index e66a282da4..c72ea716bf 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -1,4 +1,18 @@ + Предыдущий трек Следующий трек @@ -10,7 +24,7 @@ Не повторять Повторять трек Повторять все - Перемешать + Перемешать Полноэкранный режим VR-режим Скачать diff --git a/library/ui/src/main/res/values-si/strings.xml b/library/ui/src/main/res/values-si/strings.xml index b6bfb1848f..19d37854fd 100644 --- a/library/ui/src/main/res/values-si/strings.xml +++ b/library/ui/src/main/res/values-si/strings.xml @@ -1,4 +1,18 @@ + පෙර ඛණ්ඩය ඊළඟ ඛණ්ඩය @@ -10,7 +24,7 @@ කිසිවක් පුනරාවර්තනය නොකරන්න එකක් පුනරාවර්තනය කරන්න සියල්ල පුනරාවර්තනය කරන්න - කලවම් කරන්න + කලවම් කරන්න සම්පූර්ණ තිර ප්‍රකාරය VR ප්‍රකාරය බාගන්න diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index 6d5ddaea28..c45fd13dcf 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -1,4 +1,18 @@ + Predchádzajúca skladba Ďalšia skladba @@ -10,7 +24,7 @@ Neopakovať Opakovať jednu Opakovať všetko - Náhodne prehrávať + Náhodne prehrávať Režim celej obrazovky režim VR Stiahnuť diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 1e3adff704..17f1e66764 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -1,4 +1,18 @@ + Prejšnja skladba Naslednja skladba @@ -10,7 +24,7 @@ Brez ponavljanja Ponavljanje ene Ponavljanje vseh - Naključno predvajanje + Naključno predvajanje Celozaslonski način Način VR Prenos diff --git a/library/ui/src/main/res/values-sq/strings.xml b/library/ui/src/main/res/values-sq/strings.xml index d5b8903ed7..950c867e8b 100644 --- a/library/ui/src/main/res/values-sq/strings.xml +++ b/library/ui/src/main/res/values-sq/strings.xml @@ -1,4 +1,18 @@ + Kënga e mëparshme Kënga tjetër @@ -10,7 +24,7 @@ Mos përsërit asnjë Përsërit një Përsërit të gjitha - Përziej + Përziej Modaliteti me ekran të plotë Modaliteti RV Shkarko diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index b45fd8ab03..6c3074bc41 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -1,4 +1,18 @@ + Претходна песма Следећа песма @@ -10,7 +24,7 @@ Не понављај ниједну Понови једну Понови све - Пусти насумично + Пусти насумично Режим целог екрана ВР режим Преузми diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index 7af95a4632..c7dafaf786 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -1,4 +1,18 @@ + Föregående spår Nästa spår @@ -10,7 +24,7 @@ Upprepa inga Upprepa en Upprepa alla - Blanda spår + Blanda spår Helskärmsläge VR-läge Ladda ned diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 1cdd325278..66568a3acc 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -1,4 +1,18 @@ + Wimbo uliotangulia Wimbo unaofuata @@ -10,7 +24,7 @@ Usirudie yoyote Rudia moja Rudia zote - Changanya + Changanya Hali ya skrini nzima Hali ya Uhalisia Pepe Pakua diff --git a/library/ui/src/main/res/values-ta/strings.xml b/library/ui/src/main/res/values-ta/strings.xml index 2b2b9e13d6..a4544299c0 100644 --- a/library/ui/src/main/res/values-ta/strings.xml +++ b/library/ui/src/main/res/values-ta/strings.xml @@ -1,4 +1,18 @@ + முந்தைய டிராக் அடுத்த டிராக் @@ -10,7 +24,7 @@ எதையும் மீண்டும் இயக்காதே இதை மட்டும் மீண்டும் இயக்கு அனைத்தையும் மீண்டும் இயக்கு - வரிசை மாற்றி இயக்கு + வரிசை மாற்றி இயக்கு முழுத்திரைப் பயன்முறை VR பயன்முறை பதிவிறக்கும் பட்டன் diff --git a/library/ui/src/main/res/values-te/strings.xml b/library/ui/src/main/res/values-te/strings.xml index ea344b0345..8fcb29cc2f 100644 --- a/library/ui/src/main/res/values-te/strings.xml +++ b/library/ui/src/main/res/values-te/strings.xml @@ -1,4 +1,18 @@ + మునుపటి ట్రాక్ తదుపరి ట్రాక్ @@ -10,7 +24,7 @@ దేన్నీ పునరావృతం చేయకండి ఒకదాన్ని పునరావృతం చేయండి అన్నింటినీ పునరావృతం చేయండి - షఫుల్ చేయండి + షఫుల్ చేయండి పూర్తి స్క్రీన్ మోడ్ వర్చువల్ రియాలిటీ మోడ్ డౌన్‌లోడ్ చేయి diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index 3cd933ccf1..918b62f099 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -1,4 +1,18 @@ + แทร็กก่อนหน้า แทร็กถัดไป @@ -10,7 +24,7 @@ ไม่เล่นซ้ำ เล่นซ้ำเพลงเดียว เล่นซ้ำทั้งหมด - สุ่ม + สุ่ม โหมดเต็มหน้าจอ โหมด VR ดาวน์โหลด diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 21852c5011..df00a07299 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -1,4 +1,18 @@ + Nakaraang track Susunod na track @@ -10,7 +24,7 @@ Walang uulitin Mag-ulit ng isa Ulitin lahat - I-shuffle + I-shuffle Fullscreen mode VR mode I-download diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 2fbf36514f..5005f0bfb9 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -1,4 +1,18 @@ + Önceki parça Sonraki parça @@ -10,7 +24,7 @@ Hiçbirini tekrarlama Birini tekrarla Tümünü tekrarla - Karıştır + Karıştır Tam ekran modu VR modu İndir diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 5d338b61af..a42a8128b3 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -1,4 +1,18 @@ + Попередня композиція Наступна композиція @@ -10,7 +24,7 @@ Не повторювати Повторити 1 Повторити всі - Перемішати + Перемішати Повноекранний режим Режим віртуальної реальності Завантажити diff --git a/library/ui/src/main/res/values-ur/strings.xml b/library/ui/src/main/res/values-ur/strings.xml index aa98b0728e..47f35d1bae 100644 --- a/library/ui/src/main/res/values-ur/strings.xml +++ b/library/ui/src/main/res/values-ur/strings.xml @@ -1,4 +1,18 @@ + پچھلا ٹریک اگلا ٹریک @@ -10,7 +24,7 @@ کسی کو نہ دہرائیں ایک کو دہرائیں سبھی کو دہرائیں - شفل کریں + شفل کریں پوری اسکرین والی وضع VR موڈ ڈاؤن لوڈ کریں diff --git a/library/ui/src/main/res/values-uz/strings.xml b/library/ui/src/main/res/values-uz/strings.xml index 2dcf5a518d..3d8e270636 100644 --- a/library/ui/src/main/res/values-uz/strings.xml +++ b/library/ui/src/main/res/values-uz/strings.xml @@ -1,4 +1,18 @@ + Avvalgi trek Keyingi trek @@ -10,7 +24,7 @@ Takrorlanmasin Bittasini takrorlash Hammasini takrorlash - Aralash + Aralash Butun ekran rejimi VR rejimi Yuklab olish diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 1cdb249ef0..dc78b504fd 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -1,4 +1,18 @@ + Bản nhạc trước Bản nhạc tiếp theo @@ -10,7 +24,7 @@ Không lặp lại Lặp lại một Lặp lại tất cả - Phát ngẫu nhiên + Phát ngẫu nhiên Chế độ toàn màn hình Chế độ thực tế ảo Tải xuống diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index fe21669ea4..d2c3fb93ca 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -1,4 +1,18 @@ + 上一曲 下一曲 @@ -10,7 +24,7 @@ 不重复播放 重复播放一项 全部重复播放 - 随机播放 + 随机播放 全屏模式 VR 模式 下载 diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index 56e0a1a53b..d040db1b03 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -1,4 +1,18 @@ + 上一首曲目 下一首曲目 @@ -10,7 +24,7 @@ 不重複播放 重複播放單一項目 全部重複播放 - 隨機播放 + 隨機播放 全螢幕模式 虛擬現實模式 下載 diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index 7b29f7924e..c3a1b5521e 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -1,4 +1,18 @@ + 上一首曲目 下一首曲目 @@ -10,7 +24,7 @@ 不重複播放 重複播放單一項目 重複播放所有項目 - 隨機播放 + 隨機播放 全螢幕模式 虛擬實境模式 下載 diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index 83cf9b2e97..08922a5037 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -1,4 +1,18 @@ + Ithrekhi yangaphambilini Ithrekhi elandelayo @@ -10,7 +24,7 @@ Phinda okungekho Phinda okukodwa Phinda konke - Shova + Shova Imodi yesikrini esigcwele Inqubo ye-VR Landa diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index bbb4aca8d5..e3f1c3aaec 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -34,8 +34,10 @@ Repeat one Repeat all - - Shuffle + + Shuffle on + + Shuffle off Fullscreen mode diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index e73524815a..c458a3ea99 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -51,11 +51,6 @@ @string/exo_controls_pause_description - - From 8e44e3b795667ad4792d3db9bfcd23d80a158e44 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 11:42:56 +0000 Subject: [PATCH 390/424] Remove LibvpxVideoRenderer from nullness blacklist PiperOrigin-RevId: 283310946 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 7fcb89dc12..4a92859b75 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 @@ -71,8 +71,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { private final boolean enableRowMultiThreadMode; private final int threads; - private VpxDecoder decoder; - private VideoFrameMetadataListener frameMetadataListener; + @Nullable private VpxDecoder decoder; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer @@ -257,7 +257,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { TraceUtil.beginSection("createVpxDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; - decoder = + VpxDecoder decoder = new VpxDecoder( numInputBuffers, numOutputBuffers, @@ -265,6 +265,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { mediaCrypto, enableRowMultiThreadMode, threads); + this.decoder = decoder; TraceUtil.endSection(); return decoder; } From 78e72abbc47d9d2c005b49e5b17f13e415cca99c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 2 Dec 2019 13:10:01 +0000 Subject: [PATCH 391/424] Remove row VP9 multi-threading option PiperOrigin-RevId: 283319944 --- .../ext/vp9/LibvpxVideoRenderer.java | 23 ++++--------------- .../exoplayer2/ext/vp9/VpxDecoder.java | 5 ++-- 2 files changed, 7 insertions(+), 21 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 4a92859b75..c84c3b41fe 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 @@ -68,7 +68,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { */ private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; - private final boolean enableRowMultiThreadMode; private final int threads; @Nullable private VpxDecoder decoder; @@ -121,8 +120,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link - * MediaSource} factories. + * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ @Deprecated @SuppressWarnings("deprecation") @@ -140,7 +139,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys, - /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, /* numOutputBuffers= */ 4); @@ -154,7 +152,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. @@ -165,7 +162,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -176,7 +172,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, - enableRowMultiThreadMode, threads, numInputBuffers, numOutputBuffers); @@ -197,13 +192,12 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * 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 enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link - * MediaSource} factories. + * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ @Deprecated public LibvpxVideoRenderer( @@ -213,7 +207,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -224,7 +217,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys); - this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; @@ -259,12 +251,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; VpxDecoder decoder = new VpxDecoder( - numInputBuffers, - numOutputBuffers, - initialInputBufferSize, - mediaCrypto, - enableRowMultiThreadMode, - threads); + numInputBuffers, numOutputBuffers, initialInputBufferSize, mediaCrypto, threads); this.decoder = decoder; TraceUtil.endSection(); return decoder; 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 b4535a3e9c..98a26727ee 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 @@ -53,7 +53,6 @@ import java.nio.ByteBuffer; * @param initialInputBufferSize The initial size of each input buffer. * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. - * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ @@ -62,7 +61,6 @@ import java.nio.ByteBuffer; int numOutputBuffers, int initialInputBufferSize, @Nullable ExoMediaCrypto exoMediaCrypto, - boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { super( @@ -75,7 +73,8 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(/* disableLoopFilter= */ false, enableRowMultiThreadMode, threads); + vpxDecContext = + vpxInit(/* disableLoopFilter= */ false, /* enableRowMultiThreadMode= */ false, threads); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } From 02ddfdc0c824a023463ef9b42cc22bc1b2b98d7b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 13:56:44 +0000 Subject: [PATCH 392/424] Bump targetSdkVersion to 29 for demo apps only PiperOrigin-RevId: 283324612 --- constants.gradle | 3 ++- demos/cast/build.gradle | 2 +- demos/main/build.gradle | 2 +- demos/main/src/main/AndroidManifest.xml | 1 + demos/surface/build.gradle | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/constants.gradle b/constants.gradle index decb25c666..65812e4274 100644 --- a/constants.gradle +++ b/constants.gradle @@ -16,7 +16,8 @@ project.ext { releaseVersion = '2.11.0' releaseVersionCode = 2011000 minSdkVersion = 16 - targetSdkVersion = 28 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved compileSdkVersion = 29 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 69e8ddc52d..f9228e4b79 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { diff --git a/demos/main/build.gradle b/demos/main/build.gradle index d03d75f077..ab47b6de81 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 355ba43405..0240a377ac 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:requestLegacyExternalStorage="true" android:name="com.google.android.exoplayer2.demo.DemoApplication" tools:ignore="UnusedAttribute"> diff --git a/demos/surface/build.gradle b/demos/surface/build.gradle index 1f653f160e..bff05901b5 100644 --- a/demos/surface/build.gradle +++ b/demos/surface/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion 29 - targetSdkVersion 29 + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { From b296b8d80744667ab502d729fda605f7e027f5e8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 13:58:12 +0000 Subject: [PATCH 393/424] Remove nullness blacklist for UI module PiperOrigin-RevId: 283324784 --- .../android/exoplayer2/util/Assertions.java | 36 +++++ .../exoplayer2/ui/PlayerControlView.java | 44 ++++-- .../android/exoplayer2/ui/PlayerView.java | 126 ++++++++++-------- 3 files changed, 138 insertions(+), 68 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index 9a4891d329..0f3bbfa14d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -96,6 +96,42 @@ public final class Assertions { } } + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(); + } + return reference; + } + + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + return reference; + } + /** * Throws {@link NullPointerException} if {@code reference} is 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 b8642e2e42..a6636d71be 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 @@ -233,18 +233,18 @@ public class PlayerControlView extends FrameLayout { private final ComponentListener componentListener; private final CopyOnWriteArrayList visibilityListeners; - private final View previousButton; - private final View nextButton; - private final View playButton; - private final View pauseButton; - private final View fastForwardButton; - private final View rewindButton; - private final ImageView repeatToggleButton; - private final ImageView shuffleButton; - private final View vrButton; - private final TextView durationView; - private final TextView positionView; - private final TimeBar timeBar; + @Nullable private final View previousButton; + @Nullable private final View nextButton; + @Nullable private final View playButton; + @Nullable private final View pauseButton; + @Nullable private final View fastForwardButton; + @Nullable private final View rewindButton; + @Nullable private final ImageView repeatToggleButton; + @Nullable private final ImageView shuffleButton; + @Nullable private final View vrButton; + @Nullable private final TextView durationView; + @Nullable private final TextView positionView; + @Nullable private final TimeBar timeBar; private final StringBuilder formatBuilder; private final Formatter formatter; private final Timeline.Period period; @@ -299,6 +299,11 @@ public class PlayerControlView extends FrameLayout { this(context, attrs, defStyleAttr, attrs); } + @SuppressWarnings({ + "nullness:argument.type.incompatible", + "nullness:method.invocation.invalid", + "nullness:methodref.receiver.bound.invalid" + }) public PlayerControlView( Context context, @Nullable AttributeSet attrs, @@ -350,7 +355,7 @@ public class PlayerControlView extends FrameLayout { updateProgressAction = this::updateProgress; hideAction = this::hide; - LayoutInflater.from(context).inflate(controllerLayoutId, this); + LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); TimeBar customTimeBar = findViewById(R.id.exo_progress); @@ -778,6 +783,8 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow) { return; } + + @Nullable Player player = this.player; boolean enableSeeking = false; boolean enablePrevious = false; boolean enableRewind = false; @@ -809,16 +816,20 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { return; } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { repeatToggleButton.setVisibility(GONE); return; } + + @Nullable Player player = this.player; if (player == null) { setButtonEnabled(false, repeatToggleButton); repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); return; } + setButtonEnabled(true, repeatToggleButton); switch (player.getRepeatMode()) { case Player.REPEAT_MODE_OFF: @@ -843,6 +854,8 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { return; } + + @Nullable Player player = this.player; if (!showShuffleButton) { shuffleButton.setVisibility(GONE); } else if (player == null) { @@ -861,6 +874,7 @@ public class PlayerControlView extends FrameLayout { } private void updateTimeline() { + @Nullable Player player = this.player; if (player == null) { return; } @@ -935,6 +949,7 @@ public class PlayerControlView extends FrameLayout { return; } + @Nullable Player player = this.player; long position = 0; long bufferedPosition = 0; if (player != null) { @@ -985,7 +1000,7 @@ public class PlayerControlView extends FrameLayout { } } - private void setButtonEnabled(boolean enabled, View view) { + private void setButtonEnabled(boolean enabled, @Nullable View view) { if (view == null) { return; } @@ -1129,6 +1144,7 @@ public class PlayerControlView extends FrameLayout { */ public boolean dispatchMediaKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); + @Nullable Player player = this.player; if (player == null || !isHandledMediaKey(keyCode)) { return false; } 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 2e29dd3388..c55fe09f76 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 @@ -71,6 +71,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art @@ -280,19 +282,19 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4; // LINT.ThenChange(../../../../../../res/values/attrs.xml) + private final ComponentListener componentListener; @Nullable private final AspectRatioFrameLayout contentFrame; - private final View shutterView; + @Nullable private final View shutterView; @Nullable private final View surfaceView; - private final ImageView artworkView; - private final SubtitleView subtitleView; + @Nullable private final ImageView artworkView; + @Nullable private final SubtitleView subtitleView; @Nullable private final View bufferingView; @Nullable private final TextView errorMessageView; @Nullable private final PlayerControlView controller; - private final ComponentListener componentListener; @Nullable private final FrameLayout adOverlayFrameLayout; @Nullable private final FrameLayout overlayFrameLayout; - private Player player; + @Nullable private Player player; private boolean useController; @Nullable private PlayerControlView.VisibilityListener controllerVisibilityListener; private boolean useArtwork; @@ -318,9 +320,12 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider this(context, attrs, /* defStyleAttr= */ 0); } + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"}) public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + componentListener = new ComponentListener(); + if (isInEditMode()) { contentFrame = null; shutterView = null; @@ -330,7 +335,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider bufferingView = null; errorMessageView = null; controller = null; - componentListener = null; adOverlayFrameLayout = null; overlayFrameLayout = null; ImageView logo = new ImageView(context); @@ -385,7 +389,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. @@ -540,9 +543,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (this.player == player) { return; } - if (this.player != null) { - this.player.removeListener(componentListener); - Player.VideoComponent oldVideoComponent = this.player.getVideoComponent(); + @Nullable Player oldPlayer = this.player; + if (oldPlayer != null) { + oldPlayer.removeListener(componentListener); + @Nullable Player.VideoComponent oldVideoComponent = oldPlayer.getVideoComponent(); if (oldVideoComponent != null) { oldVideoComponent.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { @@ -555,13 +559,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); } } - Player.TextComponent oldTextComponent = this.player.getTextComponent(); + @Nullable Player.TextComponent oldTextComponent = oldPlayer.getTextComponent(); if (oldTextComponent != null) { oldTextComponent.removeTextOutput(componentListener); } } this.player = player; - if (useController) { + if (useController()) { controller.setPlayer(player); } if (subtitleView != null) { @@ -571,7 +575,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider updateErrorMessage(); updateForCurrentTrackSelections(/* isNewPlayer= */ true); if (player != null) { - Player.VideoComponent newVideoComponent = player.getVideoComponent(); + @Nullable Player.VideoComponent newVideoComponent = player.getVideoComponent(); if (newVideoComponent != null) { if (surfaceView instanceof TextureView) { newVideoComponent.setVideoTextureView((TextureView) surfaceView); @@ -585,7 +589,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } newVideoComponent.addVideoListener(componentListener); } - Player.TextComponent newTextComponent = player.getTextComponent(); + @Nullable Player.TextComponent newTextComponent = player.getTextComponent(); if (newTextComponent != null) { newTextComponent.addTextOutput(componentListener); } @@ -611,13 +615,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param resizeMode The {@link ResizeMode}. */ public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); contentFrame.setResizeMode(resizeMode); } /** Returns the {@link ResizeMode}. */ public @ResizeMode int getResizeMode() { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); return contentFrame.getResizeMode(); } @@ -688,7 +692,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return; } this.useController = useController; - if (useController) { + if (useController()) { controller.setPlayer(player); } else if (controller != null) { controller.hide(); @@ -793,9 +797,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return super.dispatchKeyEvent(event); } - boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController; + boolean isDpadKey = isDpadKey(event.getKeyCode()); boolean handled = false; - if (isDpadAndUseController && !controller.isVisible()) { + if (isDpadKey && useController() && !controller.isVisible()) { // Handle the key event by showing the controller. maybeShowController(true); handled = true; @@ -804,7 +808,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // controller, or extend its show timeout if already visible. maybeShowController(true); handled = true; - } else if (isDpadAndUseController) { + } else if (isDpadKey && useController()) { // The key event wasn't handled, but we should extend the controller's show timeout. maybeShowController(true); } @@ -819,7 +823,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return Whether the key event was handled. */ public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); + return useController() && controller.dispatchMediaKeyEvent(event); } /** Returns whether the controller is currently visible. */ @@ -865,7 +869,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * controller to remain visible indefinitely. */ public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); this.controllerShowTimeoutMs = controllerShowTimeoutMs; if (controller.isVisible()) { // Update the controller's timeout if necessary. @@ -884,7 +888,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. */ public void setControllerHideOnTouch(boolean controllerHideOnTouch) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); this.controllerHideOnTouch = controllerHideOnTouch; updateContentDescription(); } @@ -927,7 +931,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setControllerVisibilityListener( @Nullable PlayerControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); if (this.controllerVisibilityListener == listener) { return; } @@ -947,7 +951,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setPlaybackPreparer(playbackPreparer); } @@ -958,7 +962,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * DefaultControlDispatcher}. */ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setControlDispatcher(controlDispatcher); } @@ -969,7 +973,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * rewind button to be disabled. */ public void setRewindIncrementMs(int rewindMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setRewindIncrementMs(rewindMs); } @@ -980,7 +984,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * cause the fast forward button to be disabled. */ public void setFastForwardIncrementMs(int fastForwardMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setFastForwardIncrementMs(fastForwardMs); } @@ -990,7 +994,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. */ public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setRepeatToggleModes(repeatToggleModes); } @@ -1000,7 +1004,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param showShuffleButton Whether the shuffle button is shown. */ public void setShowShuffleButton(boolean showShuffleButton) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setShowShuffleButton(showShuffleButton); } @@ -1010,7 +1014,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param showMultiWindowTimeBar Whether to show all windows. */ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); } @@ -1026,7 +1030,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setExtraAdGroupMarkers( @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups); } @@ -1038,7 +1042,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setAspectRatioListener( @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); contentFrame.setAspectRatioListener(listener); } @@ -1089,7 +1093,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTouchEvent(MotionEvent event) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } switch (event.getAction()) { @@ -1116,7 +1120,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } maybeShowController(true); @@ -1173,7 +1177,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public ViewGroup getAdViewGroup() { - return Assertions.checkNotNull( + return Assertions.checkStateNotNull( adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback"); } @@ -1191,8 +1195,26 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. + @EnsuresNonNullIf(expression = "controller", result = true) + private boolean useController() { + if (useController) { + Assertions.checkStateNotNull(controller); + return true; + } + return false; + } + + @EnsuresNonNullIf(expression = "artworkView", result = true) + private boolean useArtwork() { + if (useArtwork) { + Assertions.checkStateNotNull(artworkView); + return true; + } + return false; + } + private boolean toggleControllerVisibility() { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } if (!controller.isVisible()) { @@ -1208,7 +1230,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (isPlayingAd() && controllerHideDuringAds) { return; } - if (useController) { + if (useController()) { boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { @@ -1229,7 +1251,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private void showController(boolean showIndefinitely) { - if (!useController) { + if (!useController()) { return; } controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); @@ -1241,6 +1263,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private void updateForCurrentTrackSelections(boolean isNewPlayer) { + @Nullable Player player = this.player; if (player == null || player.getCurrentTrackGroups().isEmpty()) { if (!keepContentOnPlayerReset) { hideArtwork(); @@ -1267,12 +1290,12 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Video disabled so the shutter must be closed. closeShutter(); // Display artwork if enabled and available, else hide it. - if (useArtwork) { + if (useArtwork()) { for (int i = 0; i < selections.length; i++) { - TrackSelection selection = selections.get(i); + @Nullable TrackSelection selection = selections.get(i); if (selection != null) { for (int j = 0; j < selection.length(); j++) { - Metadata metadata = selection.getFormat(j).metadata; + @Nullable Metadata metadata = selection.getFormat(j).metadata; if (metadata != null && setArtworkFromMetadata(metadata)) { return; } @@ -1287,6 +1310,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider hideArtwork(); } + @RequiresNonNull("artworkView") private boolean setArtworkFromMetadata(Metadata metadata) { boolean isArtworkSet = false; int currentPictureType = PICTURE_TYPE_NOT_SET; @@ -1316,6 +1340,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return isArtworkSet; } + @RequiresNonNull("artworkView") private boolean setDrawableArtwork(@Nullable Drawable drawable) { if (drawable != null) { int drawableWidth = drawable.getIntrinsicWidth(); @@ -1362,13 +1387,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider errorMessageView.setVisibility(View.VISIBLE); return; } - ExoPlaybackException error = null; - if (player != null - && player.getPlaybackState() == Player.STATE_IDLE - && errorMessageProvider != null) { - error = player.getPlaybackError(); - } - if (error != null) { + @Nullable ExoPlaybackException error = player != null ? player.getPlaybackError() : null; + if (error != null && errorMessageProvider != null) { CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second; errorMessageView.setText(errorMessage); errorMessageView.setVisibility(View.VISIBLE); @@ -1410,12 +1430,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** Applies a texture rotation to a {@link TextureView}. */ private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + Matrix transformMatrix = new Matrix(); float textureViewWidth = textureView.getWidth(); float textureViewHeight = textureView.getHeight(); - if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { - textureView.setTransform(null); - } else { - Matrix transformMatrix = new Matrix(); + if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) { float pivotX = textureViewWidth / 2; float pivotY = textureViewHeight / 2; transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); @@ -1429,8 +1447,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider textureViewHeight / rotatedTextureRect.height(), pivotX, pivotY); - textureView.setTransform(transformMatrix); } + textureView.setTransform(transformMatrix); } @SuppressLint("InlinedApi") From ab1d54d0acaadfa8b020ccf5a5bc3b32e6b2d716 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 4 Dec 2019 09:59:01 +0000 Subject: [PATCH 394/424] Merge pull request #6696 from phhusson:fix/nullable-selection-override PiperOrigin-RevId: 283347700 --- .../trackselection/DefaultTrackSelector.java | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) 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 0d74652408..437546559c 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 @@ -184,7 +184,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedRendererCapabilitiesIfNecessary; private int tunnelingAudioSessionId; - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /** @@ -646,8 +647,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { - Map overrides = selectionOverrides.get(rendererIndex); + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null) { overrides = new HashMap<>(); selectionOverrides.put(rendererIndex, overrides); @@ -669,7 +671,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || !overrides.containsKey(groups)) { // Nothing to clear. return this; @@ -688,7 +691,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || overrides.isEmpty()) { // Nothing to clear. return this; @@ -775,9 +779,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; } - private static SparseArray> cloneSelectionOverrides( - SparseArray> selectionOverrides) { - SparseArray> clone = new SparseArray<>(); + private static SparseArray> + cloneSelectionOverrides( + SparseArray> selectionOverrides) { + SparseArray> clone = + new SparseArray<>(); for (int i = 0; i < selectionOverrides.size(); i++) { clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); } @@ -962,7 +968,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int tunnelingAudioSessionId; // Overrides - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /* package */ Parameters( @@ -996,7 +1003,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedRendererCapabilitiesIfNecessary, int tunnelingAudioSessionId, // Overrides - SparseArray> selectionOverrides, + SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags) { super( preferredAudioLanguage, @@ -1087,7 +1094,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return Whether there is an override. */ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null && overrides.containsKey(groups); } @@ -1100,7 +1108,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; } @@ -1233,17 +1242,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Static utility methods. - private static SparseArray> readSelectionOverrides( - Parcel in) { + private static SparseArray> + readSelectionOverrides(Parcel in) { int renderersWithOverridesCount = in.readInt(); - SparseArray> selectionOverrides = + SparseArray> selectionOverrides = new SparseArray<>(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = in.readInt(); int overrideCount = in.readInt(); - Map overrides = new HashMap<>(overrideCount); + Map overrides = + new HashMap<>(overrideCount); for (int j = 0; j < overrideCount; j++) { - TrackGroupArray trackGroups = in.readParcelable(TrackGroupArray.class.getClassLoader()); + TrackGroupArray trackGroups = + Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader())); + @Nullable SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader()); overrides.put(trackGroups, override); } @@ -1253,16 +1265,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static void writeSelectionOverridesToParcel( - Parcel dest, SparseArray> selectionOverrides) { + Parcel dest, + SparseArray> selectionOverrides) { int renderersWithOverridesCount = selectionOverrides.size(); dest.writeInt(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = selectionOverrides.keyAt(i); - Map overrides = selectionOverrides.valueAt(i); + Map overrides = + selectionOverrides.valueAt(i); int overrideCount = overrides.size(); dest.writeInt(rendererIndex); dest.writeInt(overrideCount); - for (Map.Entry override : overrides.entrySet()) { + for (Map.Entry override : + overrides.entrySet()) { dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0); dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0); } @@ -1285,8 +1300,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - SparseArray> first, - SparseArray> second) { + SparseArray> first, + SparseArray> second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; @@ -1303,13 +1318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - Map first, - Map second) { + Map first, + Map second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; } - for (Map.Entry firstEntry : first.entrySet()) { + for (Map.Entry firstEntry : + first.entrySet()) { TrackGroupArray key = firstEntry.getKey(); if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) { return false; @@ -1536,7 +1552,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final void setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); } From 92566323da68addfd64c2103d236e444a25b2d9b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 18:24:59 +0000 Subject: [PATCH 395/424] Remove some more core classes from nullness blacklist PiperOrigin-RevId: 283366568 --- .../android/exoplayer2/NoSampleRenderer.java | 9 ++- .../audio/AudioRendererEventListener.java | 33 ++++++----- .../exoplayer2/extractor/MpegAudioHeader.java | 4 +- .../extractor/wav/WavHeaderReader.java | 2 + .../metadata/MetadataDecoderFactory.java | 31 +++++----- .../text/SubtitleDecoderFactory.java | 57 ++++++++++--------- .../android/exoplayer2/upstream/Loader.java | 36 ++++++------ .../exoplayer2/upstream/cache/CacheUtil.java | 8 +-- .../cache/LeastRecentlyUsedCacheEvictor.java | 27 ++++----- .../upstream/cache/SimpleCacheSpan.java | 13 +++-- .../video/VideoRendererEventListener.java | 36 ++++++------ 11 files changed, 138 insertions(+), 118 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 894736571c..52bf4b3d06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not @@ -27,10 +28,10 @@ import java.io.IOException; */ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { - private RendererConfiguration configuration; + @MonotonicNonNull private RendererConfiguration configuration; private int index; private int state; - private SampleStream stream; + @Nullable private SampleStream stream; private boolean streamIsFinal; @Override @@ -285,8 +286,10 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // Methods to be called by subclasses. /** - * Returns the configuration set when the renderer was most recently enabled. + * Returns the configuration set when the renderer was most recently enabled, or {@code null} if + * the renderer has never been enabled. */ + @Nullable protected final RendererConfiguration getConfiguration() { return configuration; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 042738b4f6..bf5822caf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import androidx.annotation.Nullable; @@ -105,8 +107,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */ public void enabled(final DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onAudioEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters)); } } @@ -115,11 +117,12 @@ public interface AudioRendererEventListener { */ public void decoderInitialized(final String decoderName, final long initializedTimestampMs, final long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onAudioDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } @@ -127,8 +130,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ public void inputFormatChanged(final Format format) { - if (listener != null) { - handler.post(() -> listener.onAudioInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); } } @@ -137,9 +140,11 @@ public interface AudioRendererEventListener { */ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { - if (listener != null) { + if (handler != null) { handler.post( - () -> listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); + () -> + castNonNull(listener) + .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } } @@ -148,11 +153,11 @@ public interface AudioRendererEventListener { */ public void disabled(final DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onAudioDisabled(counters); + castNonNull(listener).onAudioDisabled(counters); }); } } @@ -161,11 +166,9 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */ public void audioSessionId(final int audioSessionId) { - if (listener != null) { - handler.post(() -> listener.onAudioSessionId(audioSessionId)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } - } - } 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 e454bd51c8..8412b738bb 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; +import org.checkerframework.checker.nullness.qual.Nullable; /** * An MPEG audio frame header. @@ -195,7 +196,7 @@ public final class MpegAudioHeader { /** MPEG audio header version. */ public int version; /** The mime type. */ - public String mimeType; + @Nullable public String mimeType; /** Size of the frame associated with this header, in bytes. */ public int frameSize; /** Sample rate in samples per second. */ @@ -223,5 +224,4 @@ public final class MpegAudioHeader { this.bitrate = bitrate; this.samplesPerFrame = samplesPerFrame; } - } 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 bbcb75aa2d..97ce0c6a1e 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.wav; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.WavUtil; @@ -39,6 +40,7 @@ import java.io.IOException; * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a * supported WAV format. */ + @Nullable public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(input); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index ae4b7db5c9..0b653830a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; @@ -62,7 +63,7 @@ public interface MetadataDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) @@ -71,19 +72,23 @@ public interface MetadataDecoderFactory { @Override public MetadataDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.APPLICATION_ID3: - return new Id3Decoder(); - case MimeTypes.APPLICATION_EMSG: - return new EventMessageDecoder(); - case MimeTypes.APPLICATION_SCTE35: - return new SpliceInfoDecoder(); - case MimeTypes.APPLICATION_ICY: - return new IcyDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.APPLICATION_ID3: + return new Id3Decoder(); + case MimeTypes.APPLICATION_EMSG: + return new EventMessageDecoder(); + case MimeTypes.APPLICATION_SCTE35: + return new SpliceInfoDecoder(); + case MimeTypes.APPLICATION_ICY: + return new IcyDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index a64a1835d8..927ee8be5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; @@ -74,7 +75,7 @@ public interface SubtitleDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.TEXT_VTT.equals(mimeType) || MimeTypes.TEXT_SSA.equals(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) @@ -90,32 +91,36 @@ public interface SubtitleDecoderFactory { @Override public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel, format.initializationData); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - case MimeTypes.APPLICATION_PGS: - return new PgsDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(mimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel, format.initializationData); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 616859f047..a498f510dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -190,8 +190,8 @@ public final class Loader implements LoaderErrorThrower { private final ExecutorService downloadExecutorService; - private LoadTask currentTask; - private IOException fatalError; + @Nullable private LoadTask currentTask; + @Nullable private IOException fatalError; /** * @param threadName A name for the loader's thread. @@ -242,39 +242,34 @@ public final class Loader implements LoaderErrorThrower { */ public long startLoading( T loadable, Callback callback, int defaultMinRetryCount) { - Looper looper = Looper.myLooper(); - Assertions.checkState(looper != null); + Looper looper = Assertions.checkStateNotNull(Looper.myLooper()); fatalError = null; long startTimeMs = SystemClock.elapsedRealtime(); new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); return startTimeMs; } - /** - * Returns whether the {@link Loader} is currently loading a {@link Loadable}. - */ + /** Returns whether the loader is currently loading. */ public boolean isLoading() { return currentTask != null; } /** - * Cancels the current load. This method should only be called when a load is in progress. + * Cancels the current load. + * + * @throws IllegalStateException If the loader is not currently loading. */ public void cancelLoading() { - currentTask.cancel(false); + Assertions.checkStateNotNull(currentTask).cancel(false); } - /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. - */ + /** Releases the loader. This method should be called when the loader is no longer required. */ public void release() { release(null); } /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. + * Releases the loader. This method should be called when the loader is no longer required. * * @param callback An optional callback to be called on the loading thread once the loader has * been released. @@ -325,10 +320,10 @@ public final class Loader implements LoaderErrorThrower { private final long startTimeMs; @Nullable private Loader.Callback callback; - private IOException currentError; + @Nullable private IOException currentError; private int errorCount; - private volatile Thread executorThread; + @Nullable private volatile Thread executorThread; private volatile boolean canceled; private volatile boolean released; @@ -368,6 +363,7 @@ public final class Loader implements LoaderErrorThrower { } else { canceled = true; loadable.cancelLoad(); + Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } @@ -375,7 +371,8 @@ public final class Loader implements LoaderErrorThrower { if (released) { finish(); long nowMs = SystemClock.elapsedRealtime(); - callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + Assertions.checkNotNull(callback) + .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); // If loading, this task will be referenced from a GC root (the loading thread) until // cancellation completes. The time taken for cancellation to complete depends on the // implementation of the Loadable that the task is loading. We null the callback reference @@ -450,6 +447,7 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; + Loader.Callback callback = Assertions.checkNotNull(this.callback); if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; @@ -492,7 +490,7 @@ public final class Loader implements LoaderErrorThrower { private void execute() { currentError = null; - downloadExecutorService.execute(currentTask); + downloadExecutorService.execute(Assertions.checkNotNull(currentTask)); } private void finish() { 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 93b00718ab..ce16ea2439 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 @@ -172,7 +172,7 @@ public final class CacheUtil { @Nullable CacheKeyFactory cacheKeyFactory, CacheDataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, @@ -268,11 +268,11 @@ public final class CacheUtil { long length, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, boolean isLastBlock, - AtomicBoolean isCanceled) + @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; long initialPositionOffset = positionOffset; @@ -392,7 +392,7 @@ public final class CacheUtil { .buildCacheKey(dataSpec); } - private static void throwExceptionIfInterruptedOrCancelled(AtomicBoolean isCanceled) + private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled) throws InterruptedException { if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { throw new InterruptedException(); 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 44a735f144..c88e2643d8 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 @@ -17,13 +17,10 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; -import java.util.Comparator; import java.util.TreeSet; -/** - * Evicts least recently used cache files first. - */ -public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator { +/** Evicts least recently used cache files first. */ +public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor { private final long maxBytes; private final TreeSet leastRecentlyUsed; @@ -32,7 +29,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.maxBytes = maxBytes; - this.leastRecentlyUsed = new TreeSet<>(this); + this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare); } @Override @@ -71,16 +68,6 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar onSpanAdded(cache, newSpan); } - @Override - public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; - if (lastTouchTimestampDelta == 0) { - // Use the standard compareTo method as a tie-break. - return lhs.compareTo(rhs); - } - return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; - } - private void evictCache(Cache cache, long requiredSpace) { while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { @@ -91,4 +78,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } } + private static int compare(CacheSpan lhs, CacheSpan rhs) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { + // Use the standard compareTo method as a tie-break. + return lhs.compareTo(rhs); + } + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; + } } 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 7d9f0c9ff1..5f6ea338e6 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 @@ -116,10 +116,11 @@ import java.util.regex.Pattern; File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { - file = upgradeFile(file, index); - if (file == null) { + @Nullable File upgradedFile = upgradeFile(file, index); + if (upgradedFile == null) { return null; } + file = upgradedFile; name = file.getName(); } @@ -174,8 +175,12 @@ import java.util.regex.Pattern; key = matcher.group(1); // Keys were not escaped in version 1. } - File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + File newCacheFile = + getCacheFile( + Assertions.checkStateNotNull(file.getParentFile()), + index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), + Long.parseLong(matcher.group(3))); if (!file.renameTo(newCacheFile)) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 70f30d3280..e7dfd123b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import android.view.Surface; @@ -126,33 +128,34 @@ public interface VideoRendererEventListener { /** Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. */ public void enabled(DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onVideoEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters)); } } /** Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. */ public void decoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */ public void inputFormatChanged(Format format) { - if (listener != null) { - handler.post(() -> listener.onVideoInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format)); } } /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public void droppedFrames(int droppedFrameCount, long elapsedMs) { - if (listener != null) { - handler.post(() -> listener.onDroppedFrames(droppedFrameCount, elapsedMs)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onDroppedFrames(droppedFrameCount, elapsedMs)); } } @@ -162,29 +165,30 @@ public interface VideoRendererEventListener { int height, final int unappliedRotationDegrees, final float pixelWidthHeightRatio) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoSizeChanged( - width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); + castNonNull(listener) + .onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); } } /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */ public void renderedFirstFrame(@Nullable Surface surface) { - if (listener != null) { - handler.post(() -> listener.onRenderedFirstFrame(surface)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface)); } } /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ public void disabled(DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onVideoDisabled(counters); + castNonNull(listener).onVideoDisabled(counters); }); } } From 668e8b12e06f896649ce53362c9ddf863e71e476 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Dec 2019 11:39:03 +0000 Subject: [PATCH 396/424] Fix typo in DefaultTimeBar javadoc PiperOrigin-RevId: 283515315 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) 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 1efdeac84d..8b737bc006 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 @@ -126,35 +126,21 @@ import java.util.concurrent.CopyOnWriteArraySet; */ public class DefaultTimeBar extends View implements TimeBar { - /** - * Default height for the time bar, in dp. - */ + /** Default height for the time bar, in dp. */ public static final int DEFAULT_BAR_HEIGHT_DP = 4; - /** - * Default height for the touch target, in dp. - */ + /** Default height for the touch target, in dp. */ public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26; - /** - * Default width for ad markers, in dp. - */ + /** Default width for ad markers, in dp. */ public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4; - /** - * Default diameter for the scrubber when enabled, in dp. - */ + /** Default diameter for the scrubber when enabled, in dp. */ public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12; - /** - * Default diameter for the scrubber when disabled, in dp. - */ + /** Default diameter for the scrubber when disabled, in dp. */ public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0; - /** - * Default diameter for the scrubber when dragged, in dp. - */ + /** Default diameter for the scrubber when dragged, in dp. */ public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16; - /** - * Default color for the played portion of the time bar. - */ - public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; /** Default color for the played portion of the time bar. */ + public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; + /** Default color for the unplayed portion of the time bar. */ public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF; /** Default color for the buffered portion of the time bar. */ public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; @@ -165,19 +151,16 @@ public class DefaultTimeBar extends View implements TimeBar { /** Default color for played ad markers. */ public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00; - /** - * The threshold in dps above the bar at which touch events trigger fine scrub mode. - */ + /** The threshold in dps above the bar at which touch events trigger fine scrub mode. */ private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50; - /** - * The ratio by which times are reduced in fine scrub mode. - */ + /** The ratio by which times are reduced in fine scrub mode. */ private static final int FINE_SCRUB_RATIO = 3; /** * The time after which the scrubbing listener is notified that scrubbing has stopped after * performing an incremental scrub using key input. */ private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; + private static final int DEFAULT_INCREMENT_COUNT = 20; /** From a6098bb9fa989760f83462174896705e1a302a22 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 3 Dec 2019 14:03:17 +0000 Subject: [PATCH 397/424] Allow AdtsExtractor to encounter EOF Fixes issue:#6700 sample_cbs_truncated.adts test file produced using `$ split -b 31795 sample_truncated.adts` to remove the last 10 bytes PiperOrigin-RevId: 283530136 --- RELEASENOTES.md | 2 + .../extractor/ts/AdtsExtractor.java | 64 +- .../test/assets/ts/sample_cbs_truncated.adts | Bin 0 -> 31795 bytes .../ts/sample_cbs_truncated.adts.0.dump | 627 ++++++++++++++++++ .../ts/sample_cbs_truncated.adts.1.dump | 427 ++++++++++++ .../ts/sample_cbs_truncated.adts.2.dump | 247 +++++++ .../ts/sample_cbs_truncated.adts.3.dump | 55 ++ .../ts/sample_cbs_truncated.adts.unklen.dump | 627 ++++++++++++++++++ .../extractor/ts/AdtsExtractorTest.java | 8 + 9 files changed, 2029 insertions(+), 28 deletions(-) create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.0.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.1.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.2.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.3.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.unklen.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 373a024eea..d0ae1a3b5a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -81,6 +81,8 @@ ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Reconfigure audio sink when PCM encoding changes ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EoF when calculating average frame size + ([#6700](https://github.com/google/ExoPlayer/issues/6700)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 381f19809b..5a0973188b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerat import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -218,7 +219,7 @@ public final class AdtsExtractor implements Extractor { } scratch.skipBytes(3); int length = scratch.readSynchSafeInt(); - firstFramePosition += 10 + length; + firstFramePosition += ID3_HEADER_LENGTH + length; input.advancePeekPosition(length); } input.resetPeekPosition(); @@ -266,36 +267,43 @@ public final class AdtsExtractor implements Extractor { int numValidFrames = 0; long totalValidFramesSize = 0; - while (input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { - scratch.setPosition(0); - int syncBytes = scratch.readUnsignedShort(); - if (!AdtsReader.isAdtsSyncWord(syncBytes)) { - // Invalid sync byte pattern. - // Constant bit-rate seeking will probably fail for this stream. - numValidFrames = 0; - break; - } else { - // Read the frame size. - if (!input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { - break; - } - scratchBits.setPosition(14); - int currentFrameSize = scratchBits.readBits(13); - // Either the stream is malformed OR we're not parsing an ADTS stream. - if (currentFrameSize <= 6) { - hasCalculatedAverageFrameSize = true; - throw new ParserException("Malformed ADTS stream"); - } - totalValidFramesSize += currentFrameSize; - if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { - break; - } - if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + try { + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } } } + } catch (EOFException e) { + // We reached the end of the input during a peekFully() or advancePeekPosition() operation. + // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally + // ExtractorInput would these operations to encounter end-of-input without throwing an + // exception [internal: b/145586657]. } input.resetPeekPosition(); if (numValidFrames > 0) { diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts b/library/core/src/test/assets/ts/sample_cbs_truncated.adts new file mode 100644 index 0000000000000000000000000000000000000000..5fe02a20c74d5380ef2fbadd6d0325c1542af673 GIT binary patch literal 31795 zcma%iWl$7;*exI+0)jNs(p^&0-64(C3ew#OEG!aI(j~b_cZW0rf^?U}lG5FC z``)=9@3&>xVL0>TInVjQPv1*EgFshMk&qm0&8^JLKGE=S@(Lj#A;%#jDZmlGMec+m z{Xq((3;h51LGu56jTit$f~CSf6_|(c15J?l?>m?MoD^`caKJZJ-p|xK1TVSbPN`&C zfS^G1!mxbT0KpT}^YPp3FDU1y9qwgU+5I)Q$$>rP(`y&-!&~txMI^3wJ(mFjqn%xQ zP#lt)YZy8>V%oLk1Svxt1j_4#!DAWu&u8y|0!U>1AFf@l&cE+$9rwArAaL{sw|-s( zTC-J&FB#7xIp#I>ORSxg`+Fc^k=x>EBm=Y6AAylcApYZ-*RP1_(NeyA%uzI_dx5I{ z@Y3CfT~&9wy`i<)lUA=Z)8y++XSXx_;5ehdZ0p_))XmcVtW6IGVdFUu7j5lzLoB%C|!$Jm-YSUxY(xeVh1S?IU1s+JW*snyqC zE3k5%nH9h=*-unCH_AH}N44_vv&Ry?o4F^_fk&)<4MC(*Od_gU1a2BS>J5hg!o{Xk zw?;g|s@Ns_Q55k6OTe4_FsDjMax+<^(!J-8fXF4F$f$(hxrwpSQjsl+4VZafg#^9f z4p*fWY*(P-EGVt=<{h4O#lUHvQU7wEGjepOXMOBs3aiS7lS{ODSl-OYqlE<=y)%i8 zwW6|T-;9kci|y$igqIf^9@%ffI>9L8%P}{$$M*dj&jiU7mM7JLH6~ta6@T!KMvdBN z-M0b=+2&3&y(oPhWTG3H^P&odpd}brb>gjG*^j$aw~RpCgEOIpYc#$GpD91=V_dAv z{P@1{qMy@J1Lp9p(DiwOcbDqUY^IgaB;(ByNzGq*&+U1Z!P8CC`#*q7Vq?SY zH-8Jd+kD+~8eK_VsNp=3=!h+#q{O@4T|2m9eEn=s_1RqP$4g*j>nXwS@@)xVYUKB% zSiH4<$2TU1=k{!KxeHr`yXq?ryZ+Xi!-l7;Jv(4NwwZo6W=w7dxsE%lroL*dgjG@iqci`sTS{vSe#EdZ-$i?{- zI1m&WiHMY2fDkPh#j@*rI4x;N2pxAgwWO$O!kgaQ+K<7u>`UpXobyTkVy z(A6-@dpLoI@Q#q5V~^NpPD{aH{f8Zc3(%dIAN#IXoJNZq$NgF7(UqTPluKRmY0`Bu zlD*#i8}(N=Eed9NcP~9coJ=x%m>vGL2-zGV|LQ7}7yS6saY>q>Q#&>??DC4^G`?iCjxE{n|OAf=|Y~-BV6j55&Q|KUwA|uLSS0pD!>G@I)GtJ10Tx@vTe>73;tQU;2d@xuQUn~3)o*5IZM^W7hne~6? zcz%@(hTMi_JJ?aSunxAA?cW5$9NeJx1Go~I>Fw^P7#@m=&)D`3=Ds@J0%IHRyUR}v~ul9AvCoTw5v|x8#_`+~l1}lt?+#Xe&@fO-$a1q;F|Wqn?l_JM>4Y_{v+d4H-*A=iHh5YRekA;=3I^m=$Ug zg1+&z!B8L)7lkN}TM&yFKB;(6b6LmeX-uX-@T}qVn zbXMJ5`-=oDcD`cp%vsFJvXDFG_x4!k8W8OOiX8;)-Ot@YGp$;$`U_gg2MU+P8hz@b zj6k`+19lr+j6s>sM_tU0EqpE=Nf}A?{({z)b%hJ#y!T+pyN-T{HVm>PdkvgRwKUoh zsBdhvQh2>ag*=5gt-ikn;!Qu5u2*NGMr^e z6)rg*W4vtPog|9c1zkuO zwdx&w`^4?=#Af+Qt1sK|e7OAno@)Wn#gH5MkP6DIpyQ-;NbI8W%crQ?8XAIDfzR5B z?m;W((ftFb@U}NLrh`114%2?Px|32ycsX{HI`1pX(4 zto)*3rpEUpk;}B0G1quyG5Z`rgP3*Ou~sUI;of1+-tB4-pLM9=8|ELSd@GTkbZdh? zu@JN~wf~mBQP%6x(5nyWsnI!bd77edC=&E>0U{?xT??%K$ZkXM0_BHPJnC0Gf`et- zN>6pRWE1>K`WhDepQ}`Y(PL1*l=@4tfXr5)Q!2E-apKG)o6W$1($PD835$FYcK!Fd zAk53-D%Cg7+he{h2sf={t4N3WqY&{svN#4}1M7{aKe=4?bO%U*=zM z@0a&yF3<#A+!^%t{TjmL7#O1c^YF>85_*yzZ&z@^tFu*g}Un0%VTu?%NB?pd62Z*8rTHv$>K zaW&RI9(9=7d2OT!xBf)$&;=QFO7yn5IIPz^Sf8gm{@$Bt`5>TSRVu=Ne@`#i<$(j8 zTl4z8f7mi4$fF|fFd4|uleD6G>mUNL_H@juB;uzHB&1s&ydV5?wHE-aW0flQGeZ4@ z)-HA}oS{KC1@*#G&3wCBX7CC-_bvBvd~AUPedXnY!BHlcZ`p(@t8nqvNe!hdEy*}P z(K<6qL?t`;2`6IApgx5Y9i=C*W|ch&KNRK18w^E8Ji6&cT?XYe=6KnkEi=6Gb&Cd+ zvEoDi67pf@kjtY(U9JR;&Y{UoF2QW`*bO;%JC$xBH>N((zOfH))AYBu2f+=%JnzU6)hIkp?9h zP3#YSPukg6JET6${U|~mCh{~mozQdN!~}s4bKOSH%}lK#SR~q-rEbLP_&*ussGp%d zC!uiQp(j+W5iE5HYgrW3s=#`sD2JVH!p4Q;smcs3SSPiDx4w6J<>|?BD%|)mX_Oi3u%|5D^jUxRD4Za$)kL(0@gDjU z9WH~9PG**x-?Rs3ExsveLB~z>lxUhXPO?DCQyH)$@|F@&wjX$fbYbvP?{tdGxl)Wv zq~ZX`WtVkL_Ltc+5ajn^#OVCvhahLyCOno}4wOOw;b-ByG}_3J0z;1j2+={bNBQU* z9_4Yo|H=~bNWU`Q6JsNgB8E%2IVNflJ$LxRXenmfSJ#%7WLrWlUZ_l@(( zzOfA4{WZ|sTxyfkiq@?c$nCSn)ZE3MM#y3ROE}G$xwv0G449u?3D~|j0nJK)3S+!A z*T#%5B8%(LJF%*-bZX%w{_dcw+@e0A7+;lo@I(Q>*6ZyKmK-LPjp94iwA0E&X|%u) zx8Tc;6AHq(D+@Z@6qR8UKAy{-v zsjF^^GNScp&0&zuTBNzme8|EqIt?}ArE_Aw+n(JFo1M6 z+9gYErGNz>lJVxxqWiKvi^p8dpAQnc1cTCi=pLKjG^*BHE7m1;VX{X0y7t@5Kw;_>%?E|1nS9Qyr5_|&*q z68}hsR}_!=iFV}9&4o$Cca9Id(ZjnBMK8Gy@uk*6p8Hz>adtE}e=TAbgBe zhclmjo942$Q_9C3?weXh=C3zc{*KjrUi;N&_&R6RSi&2>Pqt^V{#&+iN+7K∈f1 zT{6X+uap!9TcvXCyyk&OV%%uM-KlIVflqVvzx|%{alem3HhSKyx)M_z%VX#{jm<=7$*xYxDsq>4>NOy{)0&jkj=o{6d9 zsqlkRI|8*m{kI&XW_vu5QoWMxyQ2j#7l^4=WJxi1dVUK>;jspFX`TI7v*7y?@13!3 zY;k}H!{612B66zwZA+cP1+RPl4)ZtLGxNpkn`XWo#icVQ3?jc;VIH7xo^J!%M2Aq($LDg#iW)k8}zU?aRPtFHzUPZCJOI(!UmDILA9~@b*9k7eL zWWP&0{}+lwh`#RhSg}-yPK-(PsC(f(VnuwQN01sbnm^3=h(^VT6mB%*8`2R`zpQbRe1%i_Qc|{jbmzt!-IjwWA~A0zUz)jZyIY;x zjsZAh$oveSiXjIMXx%VNG?5RPxp%=45~3XRAw&+K&f5e=Slb($N7sL2Ei@1D@L~}o zWcIvO-FoW-dFvaGd*no69+rNoeA*B8d7-aevK0Lm^`JD`Sn|K=nZTO&%R4{c z&hM2qr;GQK;(u`WH8{WfTBol4EKApN*&S5=mF$(mwQ5q;(aOU}^Rl!v7tjY`bW?F8 z@pdphUfGYe2PN4~wmeHJ(lLIqCYMe8MdG+2&-a*G@^CdvA7mY%S5qn$CZW3*7|`pP zAD+l#+}vy-yF15VltFP8DWx)JAXp$P=&N;ExAf{f-#3np))_#B)$fy~M4((mqy?F5J> zlr&H%DpCrUX3vzTzD?dS?B|VN>31yh-_E5_Y6DBoAa-fRKX$K5V9?2=wG-1 zl|g%CGS|O+tl%3=!f9xq&rF>0=6GLw&1dF_6*NJ#G0HmTG{3-ZQ(G=IXY#z2`b7hX?;|aq2SJAjV6$DN{>v@I-q}}MWgD9 zdzwu?2Bp5et=Z$mbS$gA^CaQ86dqWu}{eM{7{wCkyj<(yf zv`CT!9kkO){xs=)3NIz-k=Vo`G$mRz>5n-d*=1SKP^1aP2eg6Cx}+vpO9_lM9xY56 z!J59cw`)UNdvkY-KkJYB>+(FQudfmpYPO)0i4uz-hyo@kR99;oqgQf-BbeP4d3VGOxer8$Eh%OJAP?4s{T^LS>K~BYSHns1?W9* z4FB8w(X`OoQ)BHusZvYXI&(XW;z}u&uG6&yZ#=C{DBGmXL|B|cYE;7##4j?S_JzzQ zU-gvv)Kz|JJfw*^X8zZ}Z2f#YM5GT(gReKCm|1@6V==|kUIBq3oePv`kmU?JKVB7Ft#O&1{+;&8eSVU1ZkJ+-`jMr#c|i3oEOCPQ4!jge@QP z&k8jR_`Qw3nH1d`iQKF)awg7nrY3U2MukNo9c}%`r&Scf{R&YRU84LD_T{u&-pkL0 zvC4B794)P1bCnqu!st8Yh0ETt7|x5WU?y~V4`<_K`>Seo>#I`_#LAnp30oH7Aoa4hKj`BJ1xUJ zhv2vE)cYV)gUpy=t^1A{yZ+Gcegft7UeI@aW9q`rOIZT*eu@j1J2pU(&LQ`%g;&Z4 zYFu_Rb-X8wT45`a8sC!Kx=#NYc%(FgrpkrK80JP#T07Dl@)n`$+Yp%Ua{^sl@>qX@ zZPoBw>pxbulCA{bk$%pt&5Jz+1C#(4jJP6 zQ$0o2S+U#yKL1D&i{TAHs$xP&`wuqGm+oR8|C3$dkFrZ9J1gOfFl6f=;pX@l;k~q8 zLy)W)6BE{aQ_e~yO_siYt|m4jRN{wsnNHcw+xDf~V|$4KV4M|WjCg1LW6LkxN~*oSwnGQ&1ufO_@T+j( zui?4i>9!2Ha?Qy}wlL#TK(syu)G@)k^c>ylBIi<2`EQoP#-Nv3zNs_d)ISGNS+f!u@`e2QNh03{Pxn(I|be#on$Z&C^Y;G*we*Rc!m}#;5uQ z7+C|KzZ3Kkefaz!zD(zlr3F*rfQu(J4PLqC?{zIM*{+>Jp^zxLg@=Gvo0E--hv&bb z3yxEwC+GVAz6CVd_9b(~-_*J7@3f;L%O#Yx+*NNk?rD1hhoDBzR_4nKj2ou`G1ibK zd9L1D3oPSn?Ve0_9@W)R+-;eg5pVAh){HM4epgt=4Rx=I#z3a3N{xMCww|{0>(O_j*&6e0?fye+q#r>mBiSSX&uje{J+f~B^c&(8 zm(NCd3p|wOeI{YCpZ2{s9N*S`dG{Sa}SD2D2F>d`34FhZCh~C6dUvvIV~eY_Gj}H; z0imQiDt>6a+{p^K@wQ2Sfb|8;_ct5y05s?ufm~~a8hwNhSDYpU`gOB-v=uTi#(PU54X<6;z zUmD9vP=Jyf>VI4Z{8*?ts*i`mUx)*ZDk(XF^gJqQgm`GEW^ck3rDd?XohvO@rntP* zRb>-Y{#3AR2_sJ{4c}fw7oR=AJB}(Jq9j(^G|094C2&oJOGBPZ)m|y^gI5LaSfLwv zmL=?M>Dp19Gq;md*r*meFglh8zte2(Gu*-tCGqV=q53Aep|7*PI7oyXfuLM15K1`jC-@5MdXp@_y536^F?`a}I4;iiaOeFU^x#ZQ`r? z7D=jKXuq^q^!*LgX};W~MIj6>NPp=~vYgE(Ss#R%QZPg8U;QNUQ%fLDZ@RCZj-_jl zDibLdq8CfYV@u;>AnwBcWd_9?tQPGvzZvaJZS~cgKFmqT=qsI08G_Qhp?tTV4&HCm z#YE-=)01Ma!b;-@hfsj)yxH@t_#NP77}x%MU+P#h2?#aqCtiv4q-X^*SNN zQNCVXQKH}gN68w1ZCD0P#-eCVt+>8_&RopWq=`9r-0Jg^e-XwfFA&mj|Jn5tlYyLVG9A! zj)#jvsbZfZZ|nUZBAfk1VwumPOqQ;?M`!Bc)ccfe_nUW%Lp?)bqLt9yTt7=+rW07z z+*FgobrxQ!vqgpn_I#s`CAiq;OxNILk=8JuXq9|SLf}{IGYvks`ba^*(81;Ty)XD) ziVAJVj>Z==}Pz}y$M*Ch!$BZszL5+g%e5-A)&#nYW3!d3<6^>)K|F%(l z+dUviU$xj+&r&FMR_fEkF~Bn*30YY^Bk(CDsHh*?IpV^OjTqGCUEjG0u@KU$%&Vuf z0b+!;$xZR|5~g`Asg!sYs3uf)Ad7mGv(>kk!u@dbxr}aZAKEhNKW`0!B%J>`Ic}e& zR1S+pA6;JGa-~Z8RP8alZsdQ4HEHS@;#Ke!1hltms(Rip8R}jtt3SXn5r-n!yZa*uRv_)j2w#G$qmsD7ifTgvsqX{(gFR`iVBhucV$F z6OWV_iSRo!DfuI9s`-czd0wLYk}Q5t&wx0SAR=!1uB~BZygb>H=uKCICds&X)f|5U zX#UE;QmB;ytIc8yH`2f| zl8b!E%Qq!fMWd~NfyFQ5$dR}g6?C10RQTsfi+~z(J7fYvBFKnd#)o-R<7~GY!1c2VW7>QE|2Y z%{|G*DGq?++Sc72oUHxyWP!co*x39x)a!7;e~X>oPA!Gk@-#Kjg$>_V_h0txw0mK6 zby;Z1AbNI(4E@FKUU3&q?nIv_@w21#e&2wyA4lROpe1J*in#yZ(&|BfnqCfl%Wp5; zEl&k^so;z8PK#8Xg2CbWe}NyC)GRjy_ldIFHV9S|x#idQd5(WDlK-GC;9|P%2CmYm zM$?l>*gUoP$1pz{V-TDIr8lsKhF$0}tr5l;i?;~t552p3a~gwkL`X)2%M7c^0M#_r zS7%7wb=nHd_rbVj@K)`g3;fUtjWtRKY3C;N4eRI{BO*5BHeJW%GuRz@Y{?h{nz93i>A08Tt z43R{^uRnHvMTH$)zdT7!4aH>0^_2&LK7ZkfmKT9nRkefz&MeLYh=ve}^ePVutJv5{El zWm($&`K7gUr@Bd#mto+I^BnbB@a3(_YoEzBpjMHA;zTNX#r2qfw2Vt`Boi$& ztDG}D)s7%5Vr7}NbE9p?xOnz7k|@DXQWgR#F=`)zQ?ScoAr>;4VnteQjOZ&N=a_(Z z<&kBw%QJfg+vLLkOkqed0Qlaiml%7eK+KrJ&_%W?u!XR7I2-wPY-Ve@l0C5ytA}%a z|MKGCcK?3rudNBDFe{;Y#9<}uh~zC%h#;rH};#nsm zzU=C)?Nyd6>Xt1QuxG^6ra_z2m}TD59h^w!L2AqNc5>M>Wd~B8l9v+xBvd{g=R+Uu zB`)+>_$a?*UVWp-6G`U(z_5P$Sv6rcL^s)c#)euNp0~6Gz4mWy2SYA@I(80w2$d`w z{yuqgyAGhQ8>Y-w0d|1*>})TAI7xr2$VXG19A(ZVu3_Grsh-P>)@yTCG3zOS>xXR( za<-+FCA&ka!I~H5DoF|73F4a?H^0jgT7Z8n+)xMmM~bgsFMRX7ugad?nSZTWsctze zIcBD=mkF$Ji8~wA>~1WtA-!sRIh<}>$BjD@duPBO?Y~1qZ1QuLzFH}Wcfqi#iWp78 z{X#S81H%gzm$t_L=;GthMM9{Q%mg?Hl@hUZGh^XN2hrpxS|M)r-3VhM*uKEeU3Rp! zEpk@3ax%YQxaFKi-*7{ty7yp0<3i!rLV>g1L;Kyph)EucRaxCGlfHN*n1i|W7w^HD zO3~auozB^oI^>1n<6iUtTlXO}!-s4E=K^|imK~;jQ{T3?N7T9FlSck6*CU>yo8vcH zg5Rz#=zrNO(^8a)!9df`5E|{dS-?sFZ)MEK<^rJqIp- zG0jq>Dt;!P|A|qQwz2siG}nP&n(F7)QnNxd`KawkG;dsI@&EI>Q6W+po9Md>BL!jv zAvAYxgs0})56R*~0*G?Obh`|1xNNPt^a}6? z)cz>gsOQ+d_;7B%-)hO01xc9`9D|h{B(@aR@>~EV;LaQ5o)6W~9jC$9JImXnmZweM zw67{f{Y%E$^A8W!7AcqNdic_L%7i6M`rNN38y2Hkh^F<`W0HJ^tFF_DB{M$%Hqz&m z9iyEcy&sMTN4l!y-JiFo;8eLv6zOi1f~_)yzGA#g{1J>l3CoC0G4vzJjUV~(_E#0J zpVe|3S9U+ynAt1+dgOUU?T-JaQz14=!pE`uYo1veA-VscDVD?ay*l+NHI-e`nbnDVP0@aukodZBRe( z@FXSX*dExK-xG4jk?X&5MemiE2Lx1BR?lXen%-SDRW_pPHsu+=Y%bT0kH=w+zH!)8 zK&B|uMVhc}b9;5{L8rWn&2L*(^~J+^z+;EVgd59!4(We6BtnY*&dnHtfQqPIMQuz} zX)~G#Rj$eO0l)oZgP{ti{MItg^O{6KnyjB!GybnJhLBL?lor9EFR`{SBgxkk)45dmFT{HZMj9yO z+Vi+Ow<<OYLYm=C<~~^eRN0zk&vzU z;QYm0#ly?_a@h6X-VAsgMETq#*e`5Vgb^s zA*8}KTil~76YQdy+GZ%HBr?cmk-6l0@`B*Od4G0Sn&=ekcDDpOM(?Wb6J_ZZ)FGHW zvaZE}SNFMcBx7`wk#0cG^GrS_-sI>uu`EurutD!XKIGZd ziC1tj{9*T>cnNx(ya+25R%jS<+@n4~@Dfw%eK^8}@^+;%^KGF4`|J8u^^&eA|cu;1{%3(r`V#AKX~v7wMouc5s&^A3$sL!PDqeQQl0EO3mY zHNLp5CdF`~QE8@1H6cGcLmC*^&dR{tvDkXohT?bMsG^T}8&F-{7WuKi%vRH!MC#Ob z#~Jr78i&+$6Z}~bA`E)zlVgz(FnQ6g270MMOWdORFXJPQQ>$g3Z^=HE&ke=TO9;Qo zFgiBngAN^QJM)6m@vjvkA4iU@LQZQgv@c)&?<9J1Aqye)1X0gLB#SX>I?>G*RdMGIa%;wJ03s2SG z?mw(Flut}~gPR&Zh``H0UWsgiJ}Xz}w@gRag9E(7%>$|jE>08@LmastM+L<_MXgT2 zR!8(y%g-P`o7>b=iF^DbW#q2e7ALXe1AQS6D|U_U0;9j9z03)&3RrpJjdHbZqQ*dT znZS(uuOyw3YQ9-Q%oBdk+D)_sFkJgiO!?H50FmdV?_Ug|WB&V%OX1zWV+S=iBuO zJl;P+$)H*vHVLcL7wvDPjv;dTsUK(f^P6Jdp-hzT9WEH%!zk6CWBfG{=rh4-cXKT^ zalx{fEh;LXi6XzaqK#G~693j77J2<4@EoffT$C`RFnun46x4C8kcYu zaPU6-eDNT4Zx6QyRKS;wngUvdZ#_SoUER0})$^H+Q(m17RXXYVwCa=?TF>+h#9 zcPK7saeabGW{N>jU~d<2Y0@X&)3CSQWHV8>|C6^JjN;kX{TFmhCG7L?S&~qGTHx>N zZhzi~;fVRS9R=CL?8>J1>_#Co&}avGR~6;u=4WJX2~1BH1?ne^e9Q=4_L1lym@I<8 zN^!rJ)fCxcu3%+Qevy*OKtJHg3ViQ0*_=AnIcIk5+bI-qC!}S$zdLgrkm==hP^Sij z3RwQeW!U|2>-*R1o7@b*KeKJIE@R|f9TvhcP#Cq#;iPWRpbZc-Zr4{S($Huc9Nb_0 zU6Qutm@w|8%N2HJf_#FD8yb$A2Au<)@MkDnv0wiPZSfBYdD%&AG%) zIqrH!z2=9#?oX%`pU%%u*_TAnC)e`a@8?!-8I(?t_~!zXpLNsQ%y_uo>maIc1SJPW^%T zfvOs_oh0sW&ny@EXP_1)&i!j+mmag=Z3I(07&U^4?1;KuG;99$bR=PVHU$1k^;(UT zQsV9dcT=DL8k5u@Q(X`LDskB^rMMs0K?P%tiHG;6i>(gdSW680yD_RIYoY7oU0hZv z)V|S;ASI9MzgkVH3`KrrUbeoaFSLKjL$y)KAIp|DQ8SO@DzzGl0H=W#^bsuu{6Dn~fi~iChVJlUVh1scm{{zyvdEfP zq~cc)*=qj{g!wH|`{S$(!GkY+ed^$#fKDALl3u`6&u6oZn6c|C{wjSiHB%L?!lLuO zgnZQ+--%13#wPUyB-c1Lmg>H+p3m^F53p6_fSp7mG0cidL2Tu0ra!<(QI22gtKQ}0 z$e&AYBoeH0K2md2PyJXoH36{!8%}`)ZGBijbaqSOEZ@Rf5zD^zYTrEtceh$2E0zQH z2}g3(lb_WuKKO0>5WZ)b%w0ii}VA9g)10 z2!2!hDnwHzeZ$dnS!gMrfJDo<1XUS`bsu#B%Tj4AhTPw_THRcK-I3aen^YR0kzU=jV69-AMaVqv7V0X34*$ z3x>Ab?oz_4;-`QpsWHNOA>$-HR9Ic~tv8IdKe`?R_J{=G@Z)=p6&muzD#xIhmNXyr zjo;Rve$uw$5?0Fw)}dUiJ0ghoJ>;MM5@0G;2RwagS)^o#U3RlrhBb4vB;=*d_~FRj z?ar+`I-o*$FGF2$tg#GCoxBBrLB=u2M?f7;WetvMRXJdjnD=nB>b{w84pB|dvGh=F zRRX)=_sx{!<-gz9f`uPY;QS?-WzA z8PIckDmUo6&9FA2E74W5z9IecgoFfCqckLk*} zabHcpH!rJ_P;psdE!#ToZqCE=v(&oOUjZ6>5R+-hAn0^WT>HjeB^Ok6wyb}70Rk7j zR||L1m(3Sy&{U86`H}PU1nNUu-Au||kRsk@eZU|VsfMybzrr1Lgs_9HNM!Nl{(48o zyl$->vo5kQfz^1$T!o{0%cpl*kxM3#{m=MyzUk4~OY5@-g)qw}E5-xeF(-RbCGnq} z!Q>K9WI`euK2icalwQf&_w0nP(NLsb#i=3`f>wr^k!AZdPtDhEnZ)Rh#Ig=R*8!D=%UcHnu#S=p^dsTF zzp+IY_OfG}<`pQ}#4e3a51W!fetSEgCyE_&!)GB%qQMb9g)EdxDm_~$KkAa+QiHc% zMZLO|;|#DGRdM9YpD(Drys(mU2-WEu0k_-O43J}AO4_M2<-c((l^4-eE2F9MEEMH( zbwig#E&h*4Ab?>!IhG!k%Ic2}2!yD=q+a#X351PpSv^A>Y2Fezfr>2mlXRD zK%ylvHRC;e+FLhot#qTYG_(FmYVY{gA+bvRL#!vDv*o@H1GRne?b$(iN80CI8a+Vd z3zEI zty~RfZKl@d^uLqCZpBP^fS?Mlu=h7Ug0~JZQ480iaisED*j_*bm9}rIude@HTeBCi z<;s`yazr&NIK^@EMBhmwe9$@H39SFl@Au)e=&v&O?Q;|l-^f>9p;jUTt6ooXjoUBf zyxR3$l-$*aa5~$#jhJ(RwmU^EfB6`H43@uS?0bu0sh@cw-Cbs1)}Bgj^$x(I5|U9( znMVW>#+p~-;nQc=zN7h%J%OM|XN2DoR$&BcMCa!w=0kjtew9Se!*;h;o{aO>E&nxR zJnI&H>D74I_RisGEj++~mg;z4llln$+7s&Md@+~OK{fX0^8f+tFdy5LU8L+Vx^$pm zs|6q!eAKznKWyY@xzSy52C-;ihc)Kb`!YSWDVK|7H>Z1X;sxmB1v2dBZKZl9rIcYO znUa4OWdoINud*J+T)efLf^e`M(0N?H=5d!WU=-WBfHih9M?Ld8IJ&M8t{fCC=%`IlW5BG&nBE8Py|x7Q36t@;B<=^qFT+qpA&hTm4=@A*PV zyX!uBKj_p|h*r=i^Wj9I@g0>JC5d!))fZHTk#rIfS9r-!&~_z24|VqGI8M??Q1_3Ax6&)a6fid~foFT_R3 za%q6kgl_ox?=hI^yhFsnkG$MjI{kB~0(HS#PoK)r%+69Kzl*t@?&L$@>y``e@S?)K zbCuLE%GXN1Alh+WE#fGLO#NSZk~+WyKgZo&%R3u+62{R8dV`i4(ya;>8;U6F5foq* z8!Rq6h7d*Onta6R{+=?NzyD5nJ7~_`r@?WM{Q3dg2lcajJg!QXKC0;lC4QwViU?Ef z0(MuTI&XTic~?pPl;Y%|$|iZN-(Q5Cw6nN=k1EX;493@^&BoOQK9w-&5$$&uJ%n8Q z>`~8SBzr0SrBdBcZNQjz9PO@|Jz# z^lU5U-PPVEM|K-D$q%<>d7Uq0;*;b^bOF^Qo@=QnL25MbnzKW=!H6srI^f*VQTkx) zvhgKkj<*QIk_`5>*&{ab@D7BtVsyJ##1V&VA~LF%JLz6#GB;5zb9LzH{mq-I8m$3W zhmmZR3Cp%@2bjk%^p`>XW#oW?6*)uz3WJ{8-{~4O7B_8#j$2%#)AdP_Rf{;PNgWt!+$-jAlA%AJ?c)EMI z=CSyJk>0KnKUH}@@;OY`x}huh5}r;q59*-ue^?eeyO2!khL&JKU_DLAt3Un#qZ`uO8YaO4sv(Z(AHPM_Xyg8LDzs5-lg7w@ z%4}-tC)puSD8rfYNxucr1*D&D`;?~lFqjzO*?(1G&>f@W9D&ST z-oii*L(~wfgtANsAhE8|zo8;bZ6%lQ~u&Bjm^GlZBBG! zl1+5m40-#iXt{x_(2Fz0p;Z7^c1}F<=U$U4-ssl>UYjR7hB^zzf*1{IJd2Lu8mBM`KrdZpuT}e8NR1L{==;e$ z2EI7~r%dfn%HM?)uzHx{QV+Gh<%V#|XYYvr6TJC$@N_|>0X$VS;#&G2?rMZ#JQVqT z_4^QdjF1qsSKnnhEp_*MCsO`x+0|AQe?9bWCivr5Axz%(-7i+H0Os4)d8E4Eu{=5M zl_1PC|7l^XEOxf}^kfkZAJgDEZ>bP}E02F`vfC`|i9=mCu82?xNN{vTk&qd$y_p0cu--pKUWj~{S zGiz=BqG3nvRW+=3)<;DVCUnI)%8xnsROQs*$3dNmcoXn43=Kie9%olLVo)oF(qVme>esVbaZn(KWgTdESrPr=8uu-#OR0 z&ULQ)!Jnqg>3ujv%MBh6QGK%0$8bdpbvlW2Qd2MXIHfw`9-Hv6qwJK;@ZcSLLr2;pc-%T4QKd3dMHkqeQip%B4jCWu{h-^)HJRLK0j9u9Oz$HU@u z=y$2F)w2#MD{YWj;GH+Gng{DVTSE0PZjH<+4gYikm zSFyZSlFm+fs!zH8yE;lPXh>`Pz`T;aJ=$t&En~OfYF$@cmb%%_8OENIKC1jqn1Djo zko*>-0Ar~ehL=ZB1wRxU$Z@@V1UR37btZ6u@z%@Ot#n;QD)*mO=?Na)-{~^4CnOt+ z;m63y2qXw*qu)8&aMs^3+<3VqU$U$h#dunI=yCxPuvHeAs(bo`JS1pw=TNt#7=eGb z&-#$JlgcmP2_ecfV2&;>r*cYhrDRy^iWw{vd2y7OeRrwhWMj5;RaDL(i>LM7!{zXj zYo`V@w6oRt?6TsC9f>57^#T(Ww?VuuYoK|>-q0WMD6@HIV(CquCI%^O#RJ`|;a+i; zjCZVp@_J})Hld#*moJ~Dublmvaj_BxEN|^|BC$ffvh;)L@ERO9WQ+6X(CL0gI6ccpcn`W9zlB8ZnyoB?B=C#+f%ai)@YjagoAccwXxo#;5X9B6@Gx2P`LvF((7=(+~ zgRWa5&{*uCjMt9}BY3jfsV8`pTT)V=Bi~bLhnf_sLY)PzDeC+hq3?61(m7uFCC_#6bdj14J8)|W(SaeoiyX-ukmXG(=Ur_1!>B=ydShQ8{7>!vd0{WI0s_3%Vq1 zTo^w``!1_S|KXFo(A+t+>9zjQd^(jJ>0d}CHsi@6kU>WkMKvM=YHtd+A2Tp zA}Jkbwki>m5DZW3;!js8%DFq5FC7;5>t7QIX>=n^Y_}u`RsblyI)O(b{_pBcyMLk_@KVb zqw~`^>Y)Q0=MW(GvcAt65{pESf{RvQAXYK+}WZ;Kn_; zQX%DI>q%PC41{=Gk=}7>79+IW_Y3Lgxl*Iz9Ok~{^n^b$ys9fYt#q;+^)ZG7k3+~P zqf8)D|0k*^xLK4SeF|8{@ceb;2PSaR8o6P+_T%0yAzBrc*7J5+2TTI1AwTJQdUh^t zSM5g^S5D3`67wR1E0EdAgHhi#%k@|RhwToO5Vh9&3-a&+(XU*?M}e4y`pNPE z(7UJp?!6TDnsk(=u(yR=`5X_K2U&$^V%@(pj>{MN-PN9&ErVuNeU-6v)gU+wM&+_e z+QjUFmLJ_+>s*UgLW-v_+bE(wIB*}J`sF*GL98ESjdn=S6>BR*)Y+HJX;ZU1sxsD# z{jErhmy;xec$ia0=@0ntye}`Z924zwv#)sn{+^u z5;1AG^n%Y0+ltC3SySny72RKDC5T3%xvG}k2GFD$3c(Epse6JzG@{1y1+NW!jJ=Ta z2t_CT_2aLwc>Y;X_E}II|MODPzIXM<@h(n+?!}jL2V0Ks(uQVzHHvbkc0Y~HM0Ctp z!9{1-;H-WdQ%}{(-SB+ll*#`%%4K~L{Cz=JyobO4B@I4;P}=g9!fnmdtKm_W>fP;+ zt1jO5RQhW29Tx#>cuT&!C#-e#ej0KPy_b6MWa>107scIHCu%lTdp??3^8qT^m4<^D z$Zv8MIWZ5eBT@W#$0UfT6WNA!Tace!|D#HPUIU;?Zpd)}iUHU=nMI2-90$;em^)c8 z@s}~_74H_>Gy5`B1PZl*HX{PPqLI7P{qWKU2y^|+Dunbx>Hc}7s5|F%Jj;1gn3O*k z#d*MF&o`JZve&1noUQaCzthWGTUv!U^re`d2Zwom5%)6fOjl zqZr2|8m3~W{72iqgGaog9I&fy~jy zs{+e{?&M(~(>8qmM+pVUM}Q$Ez@5n8Mqpb4t6xAu?*JCcW$Yj0#U83Uo2oPwkvwir z)G*YREbj$zRW(3!z#xc=es@qr%>0yVXH`7Rsz428Eu=3o;)AXkpR{ndS^pFN(!5~_ zx}dtYWd6WzuPLx~h10JgChRn>>e4N{*^W>vT>zZH&%Kk}`XPc&QVT|v6bt4oeO+Ph zv{N0N)nX1=QcH~Zf@erSqC5Hi?{GFBOQpbGdDQQ0Mxm%@Y7&)4>iCk;`KL_XDdL?E zRz`n)-?%GZFue4ug)5F=R($oXRorx@ZOBNvg}7T)ex`s@JL)6uu0e^$KTU=CEf%}M z+&694jh)9sfEG-^kt4NdqL;43o0U4 z1S-Qq>+T(W8YnJwas4ykg8p;Rn$6)EY~XlYn@44&+c$vousrg%Gcb#JMWYIB*Y4ixNSF{f^H7`1FqYMt!@}H(MO;1NFK?k`J36Fw6rd8Gs>FC z=D!OUXuF(UI}I8nQwtiWqm!LOUTnsjfQS5dFD7O@53exQE*qdQ?+BEBs=C+pmM0!^y08@BhuQ?YQzGPJLu-?D4y+4Xw84Qn-gs@ z;)nHFxLVOx|L}caYk*yNvvwn<`*r``c&Hd6B!;Q>TGt{v*(LEIYM z7CZBSFH-q=dW0e~6C$&QqA%pZ<>|V`pM4vPj&MtdJ45SWHV&bvc{&>uYe)_{E^N)) zbrtOSGd|^FQswNalU(lB)tI^K*PxI9S@B`%uL*}QLbZ;4o!TWFGzZIlub8{)?IFmv zlW#Ihhm~xV`{SflShL^2hl#)9ma1;h$`c@~;oK0w9RGSN4RR&Ckl^Q0CZ`qAWfP19 zb-HQxO{S)DjkX{wr&MQw9d7vW{K?U2AQ!4fLkB~u9-%Gn`?H~%->ZKez7=pfzx4B3 z4_fx-cUOVTPY&v|&K*Io7CSguxB3Y8>YML&f{0joixkM8{4KR!WlsLrT?F00P$0~kSs$=c zZ%fVxxPobX`6WZl-ciT*)k4=@d0w%sZtRix&>ggTI*}P>>l88Wv_qGu21QW)^cnE2 z#rq3}Z$G>ebSvNd+_wZZZl^q z)NJ4o-Fe3i-a#+hAJ#c5<1~v+p0FVFNauk+!C;fj$(E^!Wunww zEcK8hN$PX_bQ8la%OJV^MlAl3%>d1}cG(?u{`W zKkg^f6J}nin!4iev}hz`zNV@-E>foZ$M{QuH2`%-q!BV!3GY5#k>G}7~+(azw zI;f_J)zUOpwlt*#2ZlmMKh=Aa1$`J$%jLC+WNTLIrPJPi5cJ{N?1Z;h?R2!|{_cYJ z!+CZhucpn|qNevwxbN;CyZcZO?XZoaRsvsKd5S7yyLR88C!t8!#?E4aMmMZZFi>DnR3ZAz%+Mmkt8+j_IzrPH$CoP zO8k+m5Cs3MmHTZlT#G zp9pS=7D;$s?tJ?GQ0co-*i#Nel0Ns-O2R)yyFVfcQ;j+p#Qs@!(9i)b(Blk1GY#;P zKz>nR#DAc1emLYhRAb1F_gR-Aah1_@vGUID{PH}k=3%qL&iV9K!8V^?S#2A0dDJAA z5znC)#EWQVE2*Tv3KiM%`BDNo&P03$`(y2B`f+460O1MaXb%dUL0z6~PfzhOpUTDh zHP;>GB0^0?S$`+={qoDgYl-P&(Gh)8=A=x~@b)tMuKvm2xny5-SF@Tv_EN0%O3d#7 z`~-{H@A_mo1H^05aWoK*i{Zak9>oOnpu0{#9<3hmqaw}K2DQ`}cw*~;slRCg-;2B;bLT|5h{zop#jmAZag)sZ48P1(qQ{2hQ6uyDhBj2jC-h~_ zicZCaO=%q*xd$xADMLoSEVF{g4F!Gy8-tBpO z0zA40WRF>HeS?6?uK@u3ked;xv?X4waf(r425%25NuoYUne15M$^GeTis4Hei!sux zMTi%zICR#N>j@a7KG7_pw9tu+_wf-@#SA)A*d8hdg`$1!l?CZN)t+3plr_&XJ?C^U zN&fuk&FFfj$d(lX8h}R^p?p@pV%x|1lJ94IwtVRHoxriA9Qt)U_nwU}i4Ko`|5~1R zH1qwG`WT{(OkHqHhtEAEkT&?&!Ba4?_ zmDzw@Je(2Zvtp_<3@XQCIYmq9~UcjN4_D*U)fF9Wzc(M^79 zenvkyrQAwS`ew#9!PTs!{Kf4$$uG6^gWc@vM@9oVQn-yrnU$3ETfdaj1~in}_TYFl z9kvil<5^lhx)(C6L{4%$-~`eMa6$o|R0}sXj3-w?Is$5Aay&OKUHvku(8NKwSG3GuPo1B5-r!{&t$iG(Aj~qj$oJ&l{GKmL0CSN-` zuzY6p(~B+%=5=+2M^?A}y}5SOz_kNdGfqGNdQV#FOE{4S#7l3!Osh&)@daSKmYCy6 z+oNW1jkz*)vjyAeM+9|LHJG_uZ>)vC)Q0WM_Sgg*Y}EYMXV~mK4xzF}rLC4!7N&Vx0+F z;ZI8v9Z1w)3jYNR>O98~P0_6HRUQ$L$yxA?m95pxqlOzd%Bz+((|kH!=Ug9&3_f~X z*sI>dxwYCktJm*TV=9=AN@w&yeW z!d^#jFFa%-yWKx3d^ud0|BZtqGWmZV7vkn|J?0U_56=fOM=}E`KAz4#Or*y?!PL8@ zJc&(VWRp;LAvaLfYkJC&1iXKNzN!uoT^)e1p6sBeXxMX{nz$0p@3^4LSysTEPu_Z= z&C}A>z=>N9`In9`u+xUu9h9GVwn0(db@taF?ZWHoWzcmqyu;ji)=qLX2b{8Y5Ol3N zN|5B?{jB<)*?z|;aLV)( zG2DD|A(PJ`x}s|GV-qG?f^{&U_6m-V;P5?En?dz-+euVMdd~_pQ8{L;^!2*uG-~i|($FBc`nTd_EB)wehy}6a2b}@z zB57Uw+Oz~VBy(+hOVwFu9`oA33yi;z-%D=Q*<~U~{;rN6<3~#36#_LDVhAeqYr+-%Fw`Z-4_co z6)lYe_Jv9#R39RwuAh>eXdO~rz<%wYvn!u28?RL%hVaMd#0mRPw-IZLwdguiF7KU` zAdsY|uBYhaVsG|q6$u~_c zqJD~p7dJh7RB8EpA_Q|d0{V0z8|=t%ZaZlcus|1(=XtGfX7mkKUnWrIb=iuBsb7rS z)Nltl9bdiTF?(*r-ft)FyA;JS5)mHI;MjH0z?rp~0vdK$nXjj0USxVG;Qbq%&D)Ea zrNFN&C6#+0`uPX&a;Gln1Wbp=vucEmSI&>IJ_z7mG_ z5|_OuLP1jezQ9==$Uj+B?bx$NP)zOJ;bCCmKxo#rECZpm4Fk@mkJ*5m2F*`!Q z#4RCFSJytjB1wsY1?Gr*x(i;MFIn7gG8?4!KJ~ELJF#i=@H9=`_@0tR^laoa;!Vc+ z{OsnL@+ho5Fo0ebJtpb0_`1Z7;^~0d*=f(aveh+D=y;9SQoN`e4XTIzdbgfcf z4oIEz%){>%_a`iOd2PRYEXZ2SNxA6}-)pR7&E%9}G)KmCTfTtE0FLnS&AL^`=qnZt zC6Y&B9nAi#DG^kBy3)U66*cj``AHq8U0@`kRq8fgqabUj{+V$BUgt2Uxf$)OXt)@> zR`Nq2CO+-($&&d=G{5oqbmzmXbnO4cebe0sw0w4x@I+77m_7NlF^VApO^!(vS%mBR zekjA$p&a!q)TZ$Vj}FQ6 z)@#%azqfRTFoy`8p7Ygm7cvz`zA8RSR(@ht&umPcpXvuac-9iehkeT<+$bOMZl;t^ zaO3Y-O93x0po|P*_H8mLP17n>)Yo<6(n+E&gnTq@Th+x}p=&QKMm$_j>5l`eHr@p_ zpgY-E>$7+N7Ecy5*m7mNTO?eSj5Ci``(L23rT3a?z|w;VpCcus{)?VZ2>aPpy12?? zeICu&*(G}dv*3G=jUdy7OoWH54C(88L4CM8m&qP*Wk!+{#{8}*#gXZsvB#VqZ7Gs) z(t_NjK82!^)<+g8RUdYi>V%*4zkhZ~QY9jCtm<>v#Fx4DqXT&=%*kuXqVnn%1ydt# z9vQ%Y#o&Glc^(PSUSfw8uJwxgQ7O;08Wr_5CS(q9x8RFRa~EVKXAW=}m?+%a%a>HI z`Tf=j{?*!OD^K~*f=dNV#Q(_C_#i31BVb@s*VyZN8vPf%6NBdfvDWWB@*C^Yi`mgz zG@D(+b5V7p2s}M#Fn;x+i`|c4|3e&j9(DH$thIoKp&bPfpm}Z3UYf;z_NP$GZ>^u@ zXbhnCYn!PT5YFlvd2R}?uQ9u9TfGD$_?^PL@`3hwqF1x*`Wbh*92XgJIOHt?HnthdFB22gc2|PCDGC3;Z4@QSa2lDydqqF!sZzX6h zrxz7Wbd=G;GWyjaWV6Ie8RWT^A}^=uk!!~NC#d%I5Jo6NumEvA+HOe^H!-r-&O7+W4 z7it&5ue7nJKmNOG;9G854LFg&CRnaPB;TXRkQdKB)Ag8iF%(x9E3#8pu_>jcX};VH zu64R<73(YTdKW*dXCCf;2@3|FKb?Z3ntvQAm=)%}qn-Em)=Tp)(S3Ps}c5N88YloH=`+M-r8YW5OOER;@^ zmwXm}c~6cnhubIGl2Qy*cfsD3)cw`4$}44U`%UycyEkmC;uB$ajkz79i)zh3s=ZjA%JDX@^RWG|iW{EhjnDD$hi zyj+Qkz5njHLy!8-@6aXqqP&^8dXdEV#$Hd4lY}2gA5-ahhyYiA)+ksq@I2f4Q=-Vw z{~dWa^CTK9ff8 z9iKCwOP$AmHW7YDLUEvi9{dZHJTemY|Dri3ue_G<{Uz}tPHlQABh`J2pK#6MQj5a` z53u)V+bX~n`nlXOnyFHHMG<@8mcP1z_(0$CgHQNe*J=yVGX7th)nhBZUT5bMced7Dq0C!aEG4F`Q-Q#RT)KVFr!PHs)sart6NOm^s;f-t%KwM@uj`HBh=mJlEOTCvq}t)P zof@7|hHIzguc0wKFUqgHKi<^+tEaDXQAMc3SLN)w$!J`p9Ttyu4Z6r4t zT3cCt>jW9uhdb*HgOB_%9RDS>#@&8}DCYgBkW}hn<7KC4h99pz+7!2L{4~S0xwC|g z#e64hQm7kapr=lnoK-3VvtTn5al7MX)#i*0!7-&x_EE8k;4m1PzkZ#WVC?zB$izdmv&;{SRww0XM=o5GqHJ ztH0gUa@^cIJVPKGhG%17#0WFDRWVqE{kNoP->f-Y*l&BS(mjBxHEDz#vDC@ zh`CRS$8qDod+$sgQI=93(^JjlFVH1z8RZ3tH)k?*FU4nSy>!mzRG`>N9GIOt`%!tF zGCw(-t)5&tOVID(qjuyty1Y_;}K< zNGWBW5fiQu>uc%E6@5!7rBBn;+&8(OR-=igvpL5st`u}j_N^sda89g1I%8~1TFS}> zMRT|WEm5mIo6Vz=@uEb?H)3Z_X(c7Dn$ixif>C47>8D5ZU72Ffg@`<2{TCbRqcY#P z6kb>Tq^u-WV|}ANm?d}`$6hlVELM{$WA}ycw zTGHcXA1pNfmR0&CD{ZR$EHOR`|7<5~p!}?&H7KU#aCmR4X4Pq9Iy^f8jxJ`ul$Y)p z6u&MPSi?*g_#M3pvS8ghc;7(uR(YXrIJt>jJ3WlWR=EtdAnNVg?mmQR1nWP_t{~jQ z7eoE7@--`3`f&Htkye2YD(u7SJ(Sb$AFco=u{ zx&Q!sw50fr+ZZ!ianl=_(lZnw->SN7UcDVqw@&o)JNB@il>vJk&2m-2f7&gKuS`C( zH6DG=YgAffpS9zhFP|*;lT+~wN*g7UUL1_g#5)R*HzQ@o`zMz0z#PI zockAQdUe;;%T@iI8|@3NHf;(n(t~$t!atoR9AaGtjN>l)vTdD+O(RO;d9=v$0P0bD5h49tK&yi0SC1dF8& zi-Q0^<}_cGlea3#1G`kVDl^o4ik?sW-fX68{k5iOH>;$--m0P=+Dhi9jzA2Ri1Uw~ zw%|4_=~HP&87(5Rh8H_u{;<5q!i(9syna~+yI=XIqD%_wuJ*t_^fV=i!Zr0778X`u zR>9$m*9be;7EUg`n=kHOURHQ77VMSi;$0iLHRu1~zAb)Y~ua20~}o|2`#G zhN!aQ-xgdd;HOgqKi!%|=QH0~;VX_eq2iN?%nh4)RUu3LJ0atff#RGacB;fhAbhf6 zTHVEG9Hx7T<@|(;FB?T-Gf-;O&f12=DRi9`98If98~&gD%FW)mrDKI3efZ2&QKshG zCMPv=N|?Hsr-AQ8N~4PV3#UzZA2DodwDmz#jBAs=QW|25d^0PNyw(~vS@llUlyCCu zKIHzXr&>|@k9J+Pt9x%4^D?o7*AL%kdvr1+7bpqlO7u9;(N1+gTysy{G0^;Kp!n}l zfdRqFI4}l#fEyL^E;n)ss9>dK__}O`DXog+mINM|mr$=~{or z^n`%xr;B@I5`S(Y4Ff<1-uLx7X_YL@-Y~Y_EkG16b-W$){p%hIIi0AlWT2n%#`#23 zcRtM)NrF$-=KkI5g~-C>{5Xy-S;uB~hBuoWZ%9HhiM1tG5D`Vxv;X5b@yNb#;t}F@ zbV)z@#X_vyl)jbi65CfhkX$s=r^Qe(ICY_FKH0Fi8bE1}8V)_1uQsDt=T4ct43KP1 zc0)o8tUE{dOrNTFo*y*GoM>2rfLEhT*&RVu-0?4!tw~#t^7b3VFR^za&Y4z8A{Wga z7Zo+$<6&WUnY!s(X1At-l{Q$sRUtVMlc?QAADknF#c4@WS&CF<_udN6AR>1c?glcZ z{!;tE2G!(P(KC)ZD4v{8wCqC7(5P%XidEHN>q%utq~bR+L*Y9u|5_=!8vq0lg7g4z zwa88X2H0SKqJI`eS6u3MxKO>tNUll0oA*q7j^d&SQ$1Iu2O4^+zIi-+ea~4qZXtsC z^Mi#}5cSp3tbFtO>UmP#Md1oqQG$^35r%v6TAvUNo<5RPZd0%EDu9c4b!j!&BCZZMZ^l>B`c%u#1Pj+DX?amnS2IQY|t*@b$XYZ%qI@7)id%aDvVPC6AN6)l>2 z@tb5t!JP}HHf8sGN*~qXT2|D+l5TiKl_SLb*quTjRJRJ~g!+?0^Z`GFt_HBCLIT1x@C> ztrg&rX%u!loojlZV*;ktZ5W%{=eCx>c4t^}LBCv@1|rL_n=Ky}8S}R${(4_*{|NTB zseoV3FoU*DzO%L2x?A37#Yfopgh-)KllnEUMPnbH8JpgzY_-1JBePOgdoAKQMepwM zJ4(0lJv9et3&6KfkHsXPFFDrKSh`1ol4rqR!b;MoEX%D2J@j;Sgdk>6B9VeT5+-N8 zPd(8Tu@a1?i=dY+*=Q9M;L0!S4}{cJ4K!G)W!JxLNY-YtR_QRE>#J+3bUOK7S<#6{bje-M7k--uk*kP4C$`CVT4$k@i= zO4si*d+YDU7R>IE7I(7tMmYN}fjUNxbF1G@t{W9UOk`+xbL-A zVW6fORiE*4;=#vPxlCk*skM%+YI3CLCNVB+s<(cENRFuo&iucJY7yqwHImQ;XkX@* zQM%F#_R_z>IZuPQWsEQOJ+v7W)52R7vXkYznu)3p-aA}_bp@vLCGmuaX zYTE8fw@YC${KFYEU;sgl0V*#JfDW(_zH#QmzXvqVUzmtl!=z71>b`!MjD}hkS{IG? zC*b;g_njU6_7*XX{Dhd?@`Pgcz|erFGv`C@L5ycEaX~atc7g_vzkWlZDv>=3v%cT@ z{R`21{Xb_&W6la#Y8Q$Z^3}bx@Nv6wY__kO%tcA{$YUAi58%U>+zEi+eHruxi|9ig zJ8=`~ua`)zv>>oMt*#JZ;R3G@>uYt6W3MPZHxCKzqCUQ|HIUPP1OjPSpo27)Jp0L3 zquejG9H~c3{_KyQO0mEF2Y7E{0~BmbY`0nvz{?9zZpx6ge6Z6hGBwj;WaEB?*Aj1c z=R$nJNd@s3D$#s?yxs|)k2qdXAE)7=p@F(D z)^M8SUc=8pJpF~2s`eXOFk{4@Pd-MN0=U&uA-Cu;|dhXw{*_Hps8 zDp+>q#i)6vfJlYEKRk{6H-rEJ1LTJ9ndpI?Z=k2#BM0945ezut(!a+KKdG2lEjKPN zQ!1b6n$e96oy|3af4HvuGwKN59YAu-3QixDl?*SALfAmd??v=yI7${mpF+hNa_CSa zBm0waq?pnqE-}ZPbAq@mCy3O{;b^QX#w|%KGVnbebw?2j+llB!A{w| z(Toa6aTIOXo<|cbX+-C_ZIZzY{Zs9|z9o%5!jTnW+({^YQpo!8s_>3H8Cf<}c#}q9 zU7oi>$;&4kf2V+V-uKRX;9p;y`4ak(cS#EbRKF#d~6G89*qhz0?IHRy^BGkjSsGAY`RM0 zRrv=DB@?#HdlA>~NBytnM=lAEGHt;7S18}pv2aWcjQk;5_4(`tcPVdbKM{Q0RP=q- znmg}AN@)Ffj=bUtnck<=IOX3Vu3BPVCi=92Y(^BO6x-Wofr3I|JK7ZVNwrl1Y8o&6 zlEM!T>q1B=6G^@sXt5V^8)6SfeSC(U2dMFY|Bwp!4@qtWT;jNi!Irtbmi5+YCcrL| zmyFhqWfxM@%Qd}1%1pd3dL$A#*37$yyxscY{03Ym>WKL%vZVr30LOcb; zBbpyXJtIRXX5Fqo-B;Xq>EC)9&UhBl;r%j-!pupHP%yk0G*WUn?yi~qTlT?XzUj?8 zpHWrC&sUa0Eo=mf;hG9~2LHAFL$22Vxu8p_K(2!u6AZipgzYggj?AmaB44*$CwcY5 zO_@yXiexHv<s|)tC6$^kBcp2*l*eF^P2jaT$nUi} zD|;n{ujosNxO5QQmj8JJ2gzUI8C!@%e_fuC+zCY zlo+26NDT8Uwv|o)rxKA(duaY;d`WPBo0rnQOr?~RaI)-iu+@KmUwrgTJ^4>(jgmse zQ=x-fYeMLr9+{7=O=;itz1W~rmMSU(EG)_gFRu5uB3}H)93reeDhoanCy-!au_gOL z<>BWh_i`j8|BFihokvX1fZxP&x9Z%(=kky*H+2AL5NT#LKqclubPdIHl5>{THcCS= zZMWf)1FXYk3g&GFR|*q7GWc3-;_Ag5CVmbb;a6FBKWPtahT7k}P?s2pxgT@rD@hT$ zd|h;8_ff*8+gCTxZvHZew(NfRL7f7{BqgSg*6x#8>3 z-F7!6FO`n@DY?;43lL59l2bQdFBUb~F8}C2C1>-2! new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), "ts/sample_cbs.adts"); } + + // https://github.com/google/ExoPlayer/issues/6700 + @Test + public void testSample_withSeekingAndTruncatedFile() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), + "ts/sample_cbs_truncated.adts"); + } } From ab016ebd8126d4298e159b67d51b790ce26e49d1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Dec 2019 15:47:10 +0000 Subject: [PATCH 398/424] Fix comment typo PiperOrigin-RevId: 283543456 --- .../google/android/exoplayer2/extractor/ts/AdtsExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 5a0973188b..86dacd8c30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -302,7 +302,7 @@ public final class AdtsExtractor implements Extractor { } catch (EOFException e) { // We reached the end of the input during a peekFully() or advancePeekPosition() operation. // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally - // ExtractorInput would these operations to encounter end-of-input without throwing an + // ExtractorInput would allow these operations to encounter end-of-input without throwing an // exception [internal: b/145586657]. } input.resetPeekPosition(); From 9e238eb6d365e76b20e39147aeae5091ea4451ee Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Dec 2019 16:05:27 +0000 Subject: [PATCH 399/424] MediaSessionConnector: Support ACTION_SET_CAPTIONING_ENABLED PiperOrigin-RevId: 283546707 --- RELEASENOTES.md | 2 + .../mediasession/MediaSessionConnector.java | 54 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d0ae1a3b5a..1911d32cdd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -131,6 +131,8 @@ of the extension after this change, following the instructions in the extension's readme. * Opus extension: Update to use NDK r20. +* MediaSession extension: Make media session connector dispatch + `ACTION_SET_CAPTIONING_ENABLED`. * GVR extension: This extension is now deprecated. * Demo apps (TODO: update links to point to r2.11.0 tag): * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) 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 84d5fea0c7..5382e286a1 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 @@ -339,6 +339,21 @@ public final class MediaSessionConnector { void onSetRating(Player player, RatingCompat rating, Bundle extras); } + /** Handles requests for enabling or disabling captions. */ + public interface CaptionCallback extends CommandReceiver { + + /** See {@link MediaSessionCompat.Callback#onSetCaptioningEnabled(boolean)}. */ + void onSetCaptioningEnabled(Player player, boolean enabled); + + /** + * Returns whether the media currently being played has captions. + * + *

    This method is called each time the media session playback state needs to be updated and + * published upon a player state change. + */ + boolean hasCaptions(Player player); + } + /** Handles a media button event. */ public interface MediaButtonEventHandler { /** @@ -420,6 +435,7 @@ public final class MediaSessionConnector { @Nullable private QueueNavigator queueNavigator; @Nullable private QueueEditor queueEditor; @Nullable private RatingCallback ratingCallback; + @Nullable private CaptionCallback captionCallback; @Nullable private MediaButtonEventHandler mediaButtonEventHandler; private long enabledPlaybackActions; @@ -606,7 +622,7 @@ public final class MediaSessionConnector { * * @param ratingCallback The rating callback. */ - public void setRatingCallback(RatingCallback ratingCallback) { + public void setRatingCallback(@Nullable RatingCallback ratingCallback) { if (this.ratingCallback != ratingCallback) { unregisterCommandReceiver(this.ratingCallback); this.ratingCallback = ratingCallback; @@ -614,6 +630,19 @@ public final class MediaSessionConnector { } } + /** + * Sets the {@link CaptionCallback} to handle requests to enable or disable captions. + * + * @param captionCallback The caption callback. + */ + public void setCaptionCallback(@Nullable CaptionCallback captionCallback) { + if (this.captionCallback != captionCallback) { + unregisterCommandReceiver(this.captionCallback); + this.captionCallback = captionCallback; + registerCommandReceiver(this.captionCallback); + } + } + /** * Sets a custom error on the session. * @@ -843,12 +872,14 @@ public final class MediaSessionConnector { boolean enableRewind = false; boolean enableFastForward = false; boolean enableSetRating = false; + boolean enableSetCaptioningEnabled = false; Timeline timeline = player.getCurrentTimeline(); if (!timeline.isEmpty() && !player.isPlayingAd()) { enableSeeking = player.isCurrentWindowSeekable(); enableRewind = enableSeeking && rewindMs > 0; enableFastForward = enableSeeking && fastForwardMs > 0; - enableSetRating = true; + enableSetRating = ratingCallback != null; + enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player); } long playbackActions = BASE_PLAYBACK_ACTIONS; @@ -868,9 +899,12 @@ public final class MediaSessionConnector { actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player)); } - if (ratingCallback != null && enableSetRating) { + if (enableSetRating) { actions |= PlaybackStateCompat.ACTION_SET_RATING; } + if (enableSetCaptioningEnabled) { + actions |= PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; + } return actions; } @@ -901,6 +935,13 @@ public final class MediaSessionConnector { return player != null && ratingCallback != null; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "captionCallback"}) + private boolean canDispatchSetCaptioningEnabled() { + return player != null && captionCallback != null; + } + @EnsuresNonNullIf( result = true, expression = {"player", "queueEditor"}) @@ -1353,6 +1394,13 @@ public final class MediaSessionConnector { } } + @Override + public void onSetCaptioningEnabled(boolean enabled) { + if (canDispatchSetCaptioningEnabled()) { + captionCallback.onSetCaptioningEnabled(player, enabled); + } + } + @Override public boolean onMediaButtonEvent(Intent mediaButtonEvent) { boolean isHandled = From b112ae000c9b377569fb19a782213bf6c750ec5c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 3 Dec 2019 16:51:43 +0000 Subject: [PATCH 400/424] Use peak rather than average bitrate for HLS This is a minor change ahead of merging a full variant of https://github.com/google/ExoPlayer/pull/6706, to make re-buffers less likely. Also remove variable substitution when parsing AVERAGE-BANDWIDTH (it's not required for integer attributes) PiperOrigin-RevId: 283554106 --- RELEASENOTES.md | 8 +++++--- .../source/hls/playlist/HlsPlaylistParser.java | 16 ++++++++++------ .../playlist/HlsMasterPlaylistParserTest.java | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1911d32cdd..0574445600 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -105,9 +105,11 @@ fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). -* HLS: Fix issue where streams could get stuck in an infinite buffering state - after a postroll ad - ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). * AV1 extension: * New in this release. The AV1 extension allows use of the [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index ffabbece97..993ce8e5c1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -304,12 +304,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = masterPlaylist.variants; assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); - assertThat(variants.get(1).format.bitrate).isEqualTo(1270000); + assertThat(variants.get(1).format.bitrate).isEqualTo(1280000); } @Test From e5957912452ef099be8855ba2b58995c8b31e833 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 3 Dec 2019 17:18:27 +0000 Subject: [PATCH 401/424] Clarify Cue.DIMEN_UNSET is also used for size PiperOrigin-RevId: 283559073 --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5d763ca72..bd617ad626 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 @@ -32,7 +32,7 @@ public class Cue { /** The empty cue. */ public static final Cue EMPTY = new Cue(""); - /** An unset position or width. */ + /** An unset position, width or size. */ // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero. public static final float DIMEN_UNSET = -Float.MAX_VALUE; From 5171a4bf5ed152fff38f8734eef46579035e7e29 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Dec 2019 17:42:48 +0000 Subject: [PATCH 402/424] reduce number of notification updates Issue: #6657 PiperOrigin-RevId: 283563218 --- .../ui/PlayerNotificationManager.java | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) 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 569fc93456..6c77284e46 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 @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; @@ -52,7 +53,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A notification manager to start, update and cancel a media style notification reflecting the @@ -269,14 +269,7 @@ public class PlayerNotificationManager { */ public void onBitmap(final Bitmap bitmap) { if (bitmap != null) { - mainHandler.post( - () -> { - if (player != null - && notificationTag == currentNotificationTag - && isNotificationStarted) { - startOrUpdateNotification(bitmap); - } - }); + postUpdateNotificationBitmap(bitmap, notificationTag); } } } @@ -303,6 +296,11 @@ public class PlayerNotificationManager { */ private static final String ACTION_DISMISS = "com.google.android.exoplayer.dismiss"; + // Internal messages. + + private static final int MSG_START_OR_UPDATE_NOTIFICATION = 0; + private static final int MSG_UPDATE_NOTIFICATION_BITMAP = 1; + /** * Visibility of notification on the lock screen. One of {@link * NotificationCompat#VISIBILITY_PRIVATE}, {@link NotificationCompat#VISIBILITY_PUBLIC} or {@link @@ -598,7 +596,10 @@ public class PlayerNotificationManager { controlDispatcher = new DefaultControlDispatcher(); window = new Timeline.Window(); instanceId = instanceIdCounter++; - mainHandler = new Handler(Looper.getMainLooper()); + //noinspection Convert2MethodRef + mainHandler = + Util.createHandler( + Looper.getMainLooper(), msg -> PlayerNotificationManager.this.handleMessage(msg)); notificationManager = NotificationManagerCompat.from(context); playerListener = new PlayerListener(); notificationBroadcastReceiver = new NotificationBroadcastReceiver(); @@ -662,7 +663,7 @@ public class PlayerNotificationManager { this.player = player; if (player != null) { player.addListener(playerListener); - startOrUpdateNotification(); + postStartOrUpdateNotification(); } } @@ -945,26 +946,17 @@ public class PlayerNotificationManager { /** Forces an update of the notification if already started. */ public void invalidate() { - if (isNotificationStarted && player != null) { - startOrUpdateNotification(); + if (isNotificationStarted) { + postStartOrUpdateNotification(); } } - @Nullable - private Notification startOrUpdateNotification() { - Assertions.checkNotNull(this.player); - return startOrUpdateNotification(/* bitmap= */ null); - } - - @RequiresNonNull("player") - @Nullable - private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { - Player player = this.player; + private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { boolean ongoing = getOngoing(player); builder = createNotification(player, builder, ongoing, bitmap); if (builder == null) { stopNotification(/* dismissedByUser= */ false); - return null; + return; } Notification notification = builder.build(); notificationManager.notify(notificationId, notification); @@ -975,16 +967,16 @@ public class PlayerNotificationManager { notificationListener.onNotificationStarted(notificationId, notification); } } - NotificationListener listener = notificationListener; + @Nullable NotificationListener listener = notificationListener; if (listener != null) { listener.onNotificationPosted(notificationId, notification, ongoing); } - return notification; } private void stopNotification(boolean dismissedByUser) { if (isNotificationStarted) { isNotificationStarted = false; + mainHandler.removeMessages(MSG_START_OR_UPDATE_NOTIFICATION); notificationManager.cancel(notificationId); context.unregisterReceiver(notificationBroadcastReceiver); if (notificationListener != null) { @@ -1261,6 +1253,37 @@ public class PlayerNotificationManager { && player.getPlayWhenReady(); } + private void postStartOrUpdateNotification() { + if (!mainHandler.hasMessages(MSG_START_OR_UPDATE_NOTIFICATION)) { + mainHandler.sendEmptyMessage(MSG_START_OR_UPDATE_NOTIFICATION); + } + } + + private void postUpdateNotificationBitmap(Bitmap bitmap, int notificationTag) { + mainHandler + .obtainMessage( + MSG_UPDATE_NOTIFICATION_BITMAP, notificationTag, C.INDEX_UNSET /* ignored */, bitmap) + .sendToTarget(); + } + + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_START_OR_UPDATE_NOTIFICATION: + if (player != null) { + startOrUpdateNotification(player, /* bitmap= */ null); + } + break; + case MSG_UPDATE_NOTIFICATION_BITMAP: + if (player != null && isNotificationStarted && currentNotificationTag == msg.arg1) { + startOrUpdateNotification(player, (Bitmap) msg.obj); + } + break; + default: + return false; + } + return true; + } + private static Map createPlaybackActions( Context context, int instanceId) { Map actions = new HashMap<>(); @@ -1326,37 +1349,37 @@ public class PlayerNotificationManager { @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onIsPlayingChanged(boolean isPlaying) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onTimelineChanged(Timeline timeline, int reason) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onPositionDiscontinuity(int reason) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } } From 6a354bb29fc4c0cc8a13888fb6de2de721da3ba4 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 5 Dec 2019 10:19:27 +0000 Subject: [PATCH 403/424] Merge pull request #6595 from szaboa:dev-v2-ssa-position PiperOrigin-RevId: 283722376 --- RELEASENOTES.md | 6 + constants.gradle | 1 + library/core/build.gradle | 2 + .../exoplayer2/text/ssa/SsaDecoder.java | 387 ++++++++++++++---- .../text/ssa/SsaDialogueFormat.java | 83 ++++ .../android/exoplayer2/text/ssa/SsaStyle.java | 284 +++++++++++++ .../exoplayer2/text/ssa/SsaSubtitle.java | 21 +- .../src/test/assets/ssa/invalid_positioning | 16 + .../src/test/assets/ssa/overlapping_timecodes | 12 + library/core/src/test/assets/ssa/positioning | 18 + .../assets/ssa/positioning_without_playres | 7 + library/core/src/test/assets/ssa/typical | 6 +- .../exoplayer2/text/ssa/SsaDecoderTest.java | 176 ++++++++ 13 files changed, 920 insertions(+), 99 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java create mode 100644 library/core/src/test/assets/ssa/invalid_positioning create mode 100644 library/core/src/test/assets/ssa/overlapping_timecodes create mode 100644 library/core/src/test/assets/ssa/positioning create mode 100644 library/core/src/test/assets/ssa/positioning_without_playres diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0574445600..b1b39b75b1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -83,6 +83,12 @@ ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * Allow `AdtsExtractor` to encounter EoF when calculating average frame size ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. diff --git a/constants.gradle b/constants.gradle index 65812e4274..599af54dde 100644 --- a/constants.gradle +++ b/constants.gradle @@ -20,6 +20,7 @@ project.ext { targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved compileSdkVersion = 29 dexmakerVersion = '2.21.0' + guavaVersion = '23.5-android' mockitoVersion = '2.25.0' robolectricVersion = '4.3' autoValueVersion = '1.6' diff --git a/library/core/build.gradle b/library/core/build.gradle index e145a179d9..3cc14326c5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -53,6 +53,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion + androidTestImplementation 'com.google.guava:guava:' + guavaVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion @@ -60,6 +61,7 @@ dependencies { testImplementation 'androidx.test:core:' + androidxTestCoreVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation 'com.google.truth:truth:' + truthVersion + testImplementation 'com.google.guava:guava:' + guavaVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') 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 2e78b433bd..d751772879 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,7 +15,7 @@ */ package com.google.android.exoplayer2.text.ssa; -import android.text.TextUtils; +import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -23,71 +23,90 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A {@link SimpleSubtitleDecoder} for SSA/ASS. - */ +/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */ public final class SsaDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SsaDecoder"; private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)"); - private static final String FORMAT_LINE_PREFIX = "Format: "; - private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; + + /* package */ static final String FORMAT_LINE_PREFIX = "Format:"; + /* package */ static final String STYLE_LINE_PREFIX = "Style:"; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue:"; + + private static final float DEFAULT_MARGIN = 0.05f; private final boolean haveInitializationData; + @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData; - private int formatKeyCount; - private int formatStartIndex; - private int formatEndIndex; - private int formatTextIndex; + private @MonotonicNonNull Map styles; + + /** + * The horizontal resolution used by the subtitle author - all cue positions are relative to this. + * + *

    Parsed from the {@code PlayResX} value in the {@code [Script Info]} section. + */ + private float screenWidth; + /** + * The vertical resolution used by the subtitle author - all cue positions are relative to this. + * + *

    Parsed from the {@code PlayResY} value in the {@code [Script Info]} section. + */ + private float screenHeight; public SsaDecoder() { this(/* initializationData= */ null); } /** + * Constructs an SsaDecoder with optional format & header info. + * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA * format line. The second must contain an SSA header that will be assumed common to all - * samples. + * samples. The header is everything in an SSA file before the {@code [Events]} section (i.e. + * {@code [Script Info]} and optional {@code [V4+ Styles]} section. */ public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); + screenWidth = Cue.DIMEN_UNSET; + screenHeight = Cue.DIMEN_UNSET; + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); - parseFormatLine(formatLine); + dialogueFormatFromInitializationData = + Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); parseHeader(new ParsableByteArray(initializationData.get(1))); } else { haveInitializationData = false; + dialogueFormatFromInitializationData = null; } } @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) { - ArrayList cues = new ArrayList<>(); - LongArray cueTimesUs = new LongArray(); + List> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); - - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); - long[] cueTimesUsArray = cueTimesUs.toArray(); - return new SsaSubtitle(cuesArray, cueTimesUsArray); + return new SsaSubtitle(cues, cueTimesUs); } /** @@ -98,109 +117,157 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private void parseHeader(ParsableByteArray data) { String currentLine; while ((currentLine = data.readLine()) != null) { - // TODO: Parse useful data from the header. - if (currentLine.startsWith("[Events]")) { - // We've reached the event body. + if ("[Script Info]".equalsIgnoreCase(currentLine)) { + parseScriptInfo(data); + } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { + styles = parseStyles(data); + } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { + Log.i(TAG, "[V4 Styles] are not supported"); + } else if ("[Events]".equalsIgnoreCase(currentLine)) { + // We've reached the [Events] section, so the header is over. return; } } } + /** + * Parse the {@code [Script Info]} section. + * + *

    When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} + * set to the beginning of of the first line after {@code [Script Info]}. + */ + private void parseScriptInfo(ParsableByteArray data) { + String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + String[] infoNameAndValue = currentLine.split(":"); + if (infoNameAndValue.length != 2) { + continue; + } + switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) { + case "playresx": + try { + screenWidth = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResX value. + } + break; + case "playresy": + try { + screenHeight = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResY value. + } + break; + } + } + } + + /** + * Parse the {@code [V4+ Styles]} section. + * + *

    When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing + * at the beginning of of the first line after {@code [V4+ Styles]}. + */ + private static Map parseStyles(ParsableByteArray data) { + SsaStyle.Format formatInfo = null; + Map styles = new LinkedHashMap<>(); + String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + formatInfo = SsaStyle.Format.fromFormatLine(currentLine); + } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { + if (formatInfo == null) { + Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); + continue; + } + SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + if (style != null) { + styles.put(style.name, style); + } + } + } + return styles; + } + /** * Parses the event body of the subtitle. * * @param data A {@link ParsableByteArray} from which the body should be read. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; String currentLine; while ((currentLine = data.readLine()) != null) { - if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { - parseFormatLine(currentLine); + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { - parseDialogueLine(currentLine, cues, cueTimesUs); + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); } } } - /** - * Parses a format line. - * - * @param formatLine The line to parse. - */ - private void parseFormatLine(String formatLine) { - String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); - formatKeyCount = values.length; - formatStartIndex = C.INDEX_UNSET; - formatEndIndex = C.INDEX_UNSET; - formatTextIndex = C.INDEX_UNSET; - for (int i = 0; i < formatKeyCount; i++) { - String key = Util.toLowerInvariant(values[i].trim()); - switch (key) { - case "start": - formatStartIndex = i; - break; - case "end": - formatEndIndex = i; - break; - case "text": - formatTextIndex = i; - break; - default: - // Do nothing. - break; - } - } - if (formatStartIndex == C.INDEX_UNSET - || formatEndIndex == C.INDEX_UNSET - || formatTextIndex == C.INDEX_UNSET) { - // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. - formatKeyCount = 0; - } - } - /** * Parses a dialogue line. * - * @param dialogueLine The line to parse. + * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}). + * @param format The dialogue format to use when parsing {@code dialogueLine}. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { - if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); - return; - } - - String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) - .split(",", formatKeyCount); - if (lineValues.length != formatKeyCount) { + private void parseDialogueLine( + String dialogueLine, SsaDialogueFormat format, List> cues, List cueTimesUs) { + Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX)); + String[] lineValues = + dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(",", format.length); + if (lineValues.length != format.length) { Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); return; } - long startTimeUs = parseTimecodeUs(lineValues[formatStartIndex]); + long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } - long endTimeUs = parseTimecodeUs(lineValues[formatEndIndex]); + long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]); if (endTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } + SsaStyle style = + styles != null && format.styleIndex != C.INDEX_UNSET + ? styles.get(lineValues[format.styleIndex].trim()) + : null; + String rawText = lineValues[format.textIndex]; + SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText); String text = - lineValues[formatTextIndex] - .replaceAll("\\{.*?\\}", "") // Warning that \\} can be replaced with } is bogus. + SsaStyle.Overrides.stripStyleOverrides(rawText) .replaceAll("\\\\N", "\n") .replaceAll("\\\\n", "\n"); - cues.add(new Cue(text)); - cueTimesUs.add(startTimeUs); - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); + Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + + int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); + int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); + // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue. + for (int i = startTimeIndex; i < endTimeIndex; i++) { + cues.get(i).add(cue); + } } /** @@ -209,8 +276,8 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param timeString The string to parse. * @return The parsed timestamp in microseconds. */ - public static long parseTimecodeUs(String timeString) { - Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); + private static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim()); if (!matcher.matches()) { return C.TIME_UNSET; } @@ -221,4 +288,154 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return timestampUs; } + private static Cue createCue( + String text, + @Nullable SsaStyle style, + SsaStyle.Overrides styleOverrides, + float screenWidth, + float screenHeight) { + @SsaStyle.SsaAlignment int alignment; + if (styleOverrides.alignment != SsaStyle.SsaAlignment.UNKNOWN) { + alignment = styleOverrides.alignment; + } else if (style != null) { + alignment = style.alignment; + } else { + alignment = SsaStyle.SsaAlignment.UNKNOWN; + } + @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); + @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); + + float position; + float line; + if (styleOverrides.position != null + && screenHeight != Cue.DIMEN_UNSET + && screenWidth != Cue.DIMEN_UNSET) { + position = styleOverrides.position.x / screenWidth; + line = styleOverrides.position.y / screenHeight; + } else { + // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. + position = computeDefaultLineOrPosition(positionAnchor); + line = computeDefaultLineOrPosition(lineAnchor); + } + + return new Cue( + text, + toTextAlignment(alignment), + line, + Cue.LINE_TYPE_FRACTION, + lineAnchor, + position, + positionAnchor, + /* size= */ Cue.DIMEN_UNSET); + } + + @Nullable + private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.TOP_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.TOP_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + case SsaStyle.SsaAlignment.UNKNOWN: + return null; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return null; + } + } + + @Cue.AnchorType + private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SsaAlignment.TOP_LEFT: + case SsaStyle.SsaAlignment.TOP_CENTER: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SsaAlignment.UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + @Cue.AnchorType + private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.TOP_LEFT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.TOP_CENTER: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SsaAlignment.UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) { + switch (anchor) { + case Cue.ANCHOR_TYPE_START: + return DEFAULT_MARGIN; + case Cue.ANCHOR_TYPE_MIDDLE: + return 0.5f; + case Cue.ANCHOR_TYPE_END: + return 1.0f - DEFAULT_MARGIN; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + /** + * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and + * returns the index. + * + *

    If it's inserted, we also insert a matching entry to {@code cues}. + */ + private static int addCuePlacerholderByTime( + long timeUs, List sortedCueTimesUs, List> cues) { + int insertionIndex = 0; + for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) { + if (sortedCueTimesUs.get(i) == timeUs) { + return i; + } + + if (sortedCueTimesUs.get(i) < timeUs) { + insertionIndex = i + 1; + break; + } + } + sortedCueTimesUs.add(insertionIndex, timeUs); + // Copy over cues from left, or use an empty list if we're inserting at the beginning. + cues.add( + insertionIndex, + insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1))); + return insertionIndex; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java new file mode 100644 index 0000000000..03c025cd94 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.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.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX; + +import android.text.TextUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Represents a {@code Format:} line from the {@code [Events]} section + * + *

    The indices are used to determine the location of particular properties in each {@code + * Dialogue:} line. + */ +/* package */ final class SsaDialogueFormat { + + public final int startTimeIndex; + public final int endTimeIndex; + public final int styleIndex; + public final int textIndex; + public final int length; + + private SsaDialogueFormat( + int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + this.startTimeIndex = startTimeIndex; + this.endTimeIndex = endTimeIndex; + this.styleIndex = styleIndex; + this.textIndex = textIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [Events] section. + * + * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'. + */ + @Nullable + public static SsaDialogueFormat fromFormatLine(String formatLine) { + int startTimeIndex = C.INDEX_UNSET; + int endTimeIndex = C.INDEX_UNSET; + int styleIndex = C.INDEX_UNSET; + int textIndex = C.INDEX_UNSET; + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "start": + startTimeIndex = i; + break; + case "end": + endTimeIndex = i; + break; + case "style": + styleIndex = i; + break; + case "text": + textIndex = i; + break; + } + } + return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + : null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java new file mode 100644 index 0000000000..e8070976e7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -0,0 +1,284 @@ +/* + * 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.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.PointF; +import android.text.TextUtils; +import androidx.annotation.IntDef; +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; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */ +/* package */ final class SsaStyle { + + private static final String TAG = "SsaStyle"; + + public final String name; + @SsaAlignment public final int alignment; + + private SsaStyle(String name, @SsaAlignment int alignment) { + this.name = name; + this.alignment = alignment; + } + + @Nullable + public static SsaStyle fromStyleLine(String styleLine, Format format) { + Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); + String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); + if (styleValues.length != format.length) { + Log.w( + TAG, + Util.formatInvariant( + "Skipping malformed 'Style:' line (expected %s values, found %s): '%s'", + format.length, styleValues.length, styleLine)); + return null; + } + try { + return new SsaStyle( + styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + } catch (RuntimeException e) { + Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); + return null; + } + } + + @SsaAlignment + private static int parseAlignment(String alignmentStr) { + try { + @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); + if (isValidAlignment(alignment)) { + return alignment; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); + return SsaAlignment.UNKNOWN; + } + + private static boolean isValidAlignment(@SsaAlignment int alignment) { + switch (alignment) { + case SsaAlignment.BOTTOM_CENTER: + case SsaAlignment.BOTTOM_LEFT: + case SsaAlignment.BOTTOM_RIGHT: + case SsaAlignment.MIDDLE_CENTER: + case SsaAlignment.MIDDLE_LEFT: + case SsaAlignment.MIDDLE_RIGHT: + case SsaAlignment.TOP_CENTER: + case SsaAlignment.TOP_LEFT: + case SsaAlignment.TOP_RIGHT: + return true; + case SsaAlignment.UNKNOWN: + default: + return false; + } + } + + /** + * Represents a {@code Format:} line from the {@code [V4+ Styles]} section + * + *

    The indices are used to determine the location of particular properties in each {@code + * Style:} line. + */ + /* package */ static final class Format { + + public final int nameIndex; + public final int alignmentIndex; + public final int length; + + private Format(int nameIndex, int alignmentIndex, int length) { + this.nameIndex = nameIndex; + this.alignmentIndex = alignmentIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [V4+ Styles] section. + * + * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'. + */ + @Nullable + public static Format fromFormatLine(String styleFormatLine) { + int nameIndex = C.INDEX_UNSET; + int alignmentIndex = C.INDEX_UNSET; + String[] keys = + TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "name": + nameIndex = i; + break; + case "alignment": + alignmentIndex = i; + break; + } + } + return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + } + } + + /** + * Represents the style override information parsed from an SSA/ASS dialogue line. + * + *

    Overrides are contained in braces embedded in the dialogue text of the cue. + */ + /* package */ static final class Overrides { + + private static final String TAG = "SsaStyle.Overrides"; + + /** Matches "{foo}" and returns "foo" in group 1 */ + // Warning that \\} can be replaced with } is bogus [internal: b/144480183]. + private static final Pattern BRACES_PATTERN = Pattern.compile("\\{([^}]*)\\}"); + + private static final String PADDED_DECIMAL_PATTERN = "\\s*\\d+(?:\\.\\d+)?\\s*"; + + /** Matches "\pos(x,y)" and returns "x" in group 1 and "y" in group 2 */ + private static final Pattern POSITION_PATTERN = + Pattern.compile(Util.formatInvariant("\\\\pos\\((%1$s),(%1$s)\\)", PADDED_DECIMAL_PATTERN)); + /** Matches "\move(x1,y1,x2,y2[,t1,t2])" and returns "x2" in group 1 and "y2" in group 2 */ + private static final Pattern MOVE_PATTERN = + Pattern.compile( + Util.formatInvariant( + "\\\\move\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\)", PADDED_DECIMAL_PATTERN)); + + /** Matches "\anx" and returns x in group 1 */ + private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile("\\\\an(\\d+)"); + + @SsaAlignment public final int alignment; + @Nullable public final PointF position; + + private Overrides(@SsaAlignment int alignment, @Nullable PointF position) { + this.alignment = alignment; + this.position = position; + } + + public static Overrides parseFromDialogue(String text) { + @SsaAlignment int alignment = SsaAlignment.UNKNOWN; + PointF position = null; + Matcher matcher = BRACES_PATTERN.matcher(text); + while (matcher.find()) { + String braceContents = matcher.group(1); + try { + PointF parsedPosition = parsePosition(braceContents); + if (parsedPosition != null) { + position = parsedPosition; + } + } catch (RuntimeException e) { + // Ignore invalid \pos() or \move() function. + } + try { + @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents); + if (parsedAlignment != SsaAlignment.UNKNOWN) { + alignment = parsedAlignment; + } + } catch (RuntimeException e) { + // Ignore invalid \an alignment override. + } + } + return new Overrides(alignment, position); + } + + public static String stripStyleOverrides(String dialogueLine) { + return BRACES_PATTERN.matcher(dialogueLine).replaceAll(""); + } + + /** + * Parses the position from a style override, returns null if no position is found. + * + *

    The attribute is expected to be in the form {@code \pos(x,y)} or {@code + * \move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of + * {@code \move()}, this returns {@code (x2, y2)} (i.e. the end position of the move). + * + * @param styleOverride The string to parse. + * @return The parsed position, or null if no position is found. + */ + @Nullable + private static PointF parsePosition(String styleOverride) { + Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride); + Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride); + boolean hasPosition = positionMatcher.find(); + boolean hasMove = moveMatcher.find(); + + String x; + String y; + if (hasPosition) { + if (hasMove) { + Log.i( + TAG, + "Override has both \\pos(x,y) and \\move(x1,y1,x2,y2); using \\pos values. override='" + + styleOverride + + "'"); + } + x = positionMatcher.group(1); + y = positionMatcher.group(2); + } else if (hasMove) { + x = moveMatcher.group(1); + y = moveMatcher.group(2); + } else { + return null; + } + return new PointF( + Float.parseFloat(Assertions.checkNotNull(x).trim()), + Float.parseFloat(Assertions.checkNotNull(y).trim())); + } + + @SsaAlignment + private static int parseAlignmentOverride(String braceContents) { + Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); + return matcher.find() ? parseAlignment(matcher.group(1)) : SsaAlignment.UNKNOWN; + } + } + + /** The SSA/ASS alignments. */ + @IntDef({ + SsaAlignment.UNKNOWN, + SsaAlignment.BOTTOM_LEFT, + SsaAlignment.BOTTOM_CENTER, + SsaAlignment.BOTTOM_RIGHT, + SsaAlignment.MIDDLE_LEFT, + SsaAlignment.MIDDLE_CENTER, + SsaAlignment.MIDDLE_RIGHT, + SsaAlignment.TOP_LEFT, + SsaAlignment.TOP_CENTER, + SsaAlignment.TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + /* package */ @interface SsaAlignment { + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + int UNKNOWN = -1; + int BOTTOM_LEFT = 1; + int BOTTOM_CENTER = 2; + int BOTTOM_RIGHT = 3; + int MIDDLE_LEFT = 4; + int MIDDLE_CENTER = 5; + int MIDDLE_RIGHT = 6; + int TOP_LEFT = 7; + int TOP_CENTER = 8; + int TOP_RIGHT = 9; + } +} 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 9a3756194f..4093f7974d 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 @@ -28,14 +28,14 @@ import java.util.List; */ /* package */ final class SsaSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List> cues; + private final List cueTimesUs; /** * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ - public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + public SsaSubtitle(List> cues, List cueTimesUs) { this.cues = cues; this.cueTimesUs = cueTimesUs; } @@ -43,30 +43,29 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : C.INDEX_UNSET; + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { - return cueTimesUs.length; + return cueTimesUs.size(); } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); } @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == Cue.EMPTY) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. + if (index == -1) { + // timeUs is earlier than the start of the first cue. return Collections.emptyList(); } else { - return Collections.singletonList(cues[index]); + return cues.get(index); } } - } diff --git a/library/core/src/test/assets/ssa/invalid_positioning b/library/core/src/test/assets/ssa/invalid_positioning new file mode 100644 index 0000000000..ade4cce9c4 --- /dev/null +++ b/library/core/src/test/assets/ssa/invalid_positioning @@ -0,0 +1,16 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 200 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(-5,50)}First subtitle (negative \pos()). +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,{\move(-5,50,-5,50)}Second subtitle (negative \move()). +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\an11}Third subtitle (invalid alignment). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,\pos(150,100) Fourth subtitle (no braces). diff --git a/library/core/src/test/assets/ssa/overlapping_timecodes b/library/core/src/test/assets/ssa/overlapping_timecodes new file mode 100644 index 0000000000..2093a96ac5 --- /dev/null +++ b/library/core/src/test/assets/ssa/overlapping_timecodes @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[Events] +Format: Start, End, Text +Dialogue: 0:00:01.00,0:00:04.23,First subtitle - end overlaps second +Dialogue: 0:00:02.00,0:00:05.23,Second subtitle - beginning overlaps first +Dialogue: 0:00:08.44,0:00:09.44,Fourth subtitle - same timings as fifth +Dialogue: 0:00:06.00,0:00:08.44,Third subtitle - out of order +Dialogue: 0:00:08.44,0:00:09.44,Fifth subtitle - same timings as fourth +Dialogue: 0:00:10.72,0:00:15.65,Sixth subtitle - fully encompasses seventh +Dialogue: 0:00:13.22,0:00:14.22,Seventh subtitle - nested fully inside sixth diff --git a/library/core/src/test/assets/ssa/positioning b/library/core/src/test/assets/ssa/positioning new file mode 100644 index 0000000000..af19fc3724 --- /dev/null +++ b/library/core/src/test/assets/ssa/positioning @@ -0,0 +1,18 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 202 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50.5)}First subtitle. +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,Second subtitle{\pos(75,50.5)}. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\pos(150,100)}Third subtitle{\pos(75,101)}, (only last counts). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,{\move(150,100,150,50.5)}Fourth subtitle. +Dialogue: 0,0:00:13:56,0:00:15:90,Default,Olly,{ \pos( 150, 101 ) }Fifth subtitle {\an2}(alignment override, spaces around pos arguments). +Dialogue: 0,0:00:16:56,0:00:19:90,Default,Olly,{\pos(150,101)\an9}Sixth subtitle (multiple overrides in same braces). diff --git a/library/core/src/test/assets/ssa/positioning_without_playres b/library/core/src/test/assets/ssa/positioning_without_playres new file mode 100644 index 0000000000..75b7967b34 --- /dev/null +++ b/library/core/src/test/assets/ssa/positioning_without_playres @@ -0,0 +1,7 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50)}First subtitle. diff --git a/library/core/src/test/assets/ssa/typical b/library/core/src/test/assets/ssa/typical index 4542af1217..3d36503251 100644 --- a/library/core/src/test/assets/ssa/typical +++ b/library/core/src/test/assets/ssa/typical @@ -7,6 +7,6 @@ Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000 [Events] Format: Layer, Start, End, Style, Name, Text -Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. -Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another. -Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. +Dialogue: 0,0:00:00.00,0:00:01.23,Default ,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,0:00:03.45,Default ,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default ,Olly,This is the third subtitle, with a comma. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 3c48aa61dd..9112bec398 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -16,11 +16,15 @@ package com.google.android.exoplayer2.text.ssa; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import android.text.Layout; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; +import com.google.common.collect.Iterables; import java.io.IOException; import java.util.ArrayList; import org.junit.Test; @@ -35,7 +39,11 @@ public final class SsaDecoderTest { private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header"; private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; + private static final String OVERLAPPING_TIMECODES = "ssa/overlapping_timecodes"; + private static final String POSITIONS = "ssa/positioning"; private static final String INVALID_TIMECODES = "ssa/invalid_timecodes"; + private static final String INVALID_POSITIONS = "ssa/invalid_positioning"; + private static final String POSITIONS_WITHOUT_PLAYRES = "ssa/positioning_without_playres"; @Test public void testDecodeEmpty() throws IOException { @@ -54,6 +62,19 @@ public final class SsaDecoderTest { Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center). + Cue firstCue = subtitle.getCues(subtitle.getEventTime(0)).get(0); + assertWithMessage("Cue.textAlignment") + .that(firstCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertWithMessage("Cue.positionAnchor") + .that(firstCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.lineAnchor").that(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.95f); + assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); assertTypicalCue3(subtitle, 4); @@ -79,6 +100,161 @@ public final class SsaDecoderTest { assertTypicalCue3(subtitle, 4); } + @Test + public void testDecodeOverlappingTimecodes() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(subtitle.getEventTime(2)).isEqualTo(4_230_000); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_230_000); + assertThat(subtitle.getEventTime(4)).isEqualTo(6_000_000); + assertThat(subtitle.getEventTime(5)).isEqualTo(8_440_000); + assertThat(subtitle.getEventTime(6)).isEqualTo(9_440_000); + assertThat(subtitle.getEventTime(7)).isEqualTo(10_720_000); + assertThat(subtitle.getEventTime(8)).isEqualTo(13_220_000); + assertThat(subtitle.getEventTime(9)).isEqualTo(14_220_000); + assertThat(subtitle.getEventTime(10)).isEqualTo(15_650_000); + + String firstSubtitleText = "First subtitle - end overlaps second"; + String secondSubtitleText = "Second subtitle - beginning overlaps first"; + String thirdSubtitleText = "Third subtitle - out of order"; + String fourthSubtitleText = "Fourth subtitle - same timings as fifth"; + String fifthSubtitleText = "Fifth subtitle - same timings as fourth"; + String sixthSubtitleText = "Sixth subtitle - fully encompasses seventh"; + String seventhSubtitleText = "Seventh subtitle - nested fully inside sixth"; + assertThat(Iterables.transform(subtitle.getCues(1_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(2_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText, secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(4_230_010), cue -> cue.text.toString())) + .containsExactly(secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(5_230_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(6_000_010), cue -> cue.text.toString())) + .containsExactly(thirdSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(8_440_010), cue -> cue.text.toString())) + .containsExactly(fourthSubtitleText, fifthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(9_440_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(10_720_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(13_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText, seventhSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(14_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(15_650_010), cue -> cue.text.toString())) + .isEmpty(); + } + + @Test + public void testDecodePositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Check \pos() sets position & line + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.25f); + + // Check the \pos() doesn't need to be at the start of the line. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.25f); + assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.25f); + + // Check only the last \pos() value is used. + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertWithMessage("Cue.position").that(thirdCue.position).isEqualTo(0.25f); + + // Check \move() is treated as \pos() + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.25f); + + // Check alignment override in a separate brace (to bottom-center) affects textAlignment and + // both line & position anchors. + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertWithMessage("Cue.position").that(fifthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(fifthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(fifthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.lineAnchor").that(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.textAlignment") + .that(fifthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + + // Check alignment override in the same brace (to top-right) affects textAlignment and both line + // & position anchors. + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertWithMessage("Cue.position").that(sixthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(sixthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(sixthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.lineAnchor").that(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertWithMessage("Cue.textAlignment") + .that(sixthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + } + + @Test + public void testDecodeInvalidPositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment. + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.5f); + + // Negative parameter to \move() - fall back to the positions implied by middle-left alignment. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.5f); + + // Check invalid alignment override (11) is skipped and style-provided one is used (4). + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertWithMessage("Cue.positionAnchor") + .that(thirdCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + assertWithMessage("Cue.lineAnchor").that(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.textAlignment") + .that(thirdCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_NORMAL); + + // No braces - fall back to the positions implied by middle-left alignment + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.5f); + } + + @Test + public void testDecodePositionsWithMissingPlayResY() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't + // set (so we don't know the denominator). + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + } + @Test public void testDecodeInvalidTimecodes() throws IOException { // Parsing should succeed, parsing the third cue only. From 86a86f6466987f97954e16a0bc0b6d256fa53944 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 4 Dec 2019 11:41:38 +0000 Subject: [PATCH 404/424] Refactor ExtractorInput javadoc about allowEndOfInput This parameter is a little confusing, especially as the behaviour can be surprising if the intended use-case isn't clear. This change moves the description of the parameter into the class javadoc, adds context/justification and slims down each method's javadoc to refer to the class-level. Related to investigating/fixing issue:#6700 PiperOrigin-RevId: 283724826 --- .../exoplayer2/extractor/ExtractorInput.java | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 45650c45fa..1b492e38c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -18,9 +18,50 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; /** * Provides data to be consumed by an {@link Extractor}. + * + *

    This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + *

      + *
    • The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level + * access operations. + *
    • The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + *
    + * + *

    {@link InputStream}-like methods

    + * + *

    The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level + * access operations. The {@code length} parameter is a maximum, and each method returns the number + * of bytes actually processed. This may be less than {@code length} because the end of the input + * was reached, or the method was interrupted, or the operation was aborted early for another + * reason. + * + *

    Block-based methods

    + * + *

    The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + *

    These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's not intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + *

    The expected behaviour of the block-based methods is therefore: + * + *

      + *
    • Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + *
    • Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + *
    • Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + *
    */ public interface ExtractorInput { @@ -41,22 +82,16 @@ public interface ExtractorInput { /** * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. - *

    - * If the end of the input is found having read no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

    - * Encountering the end of input having partially satisfied the read is always considered an - * error, and will result in an {@link EOFException} being thrown. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the read was successful. False if the end of the input was encountered having - * read no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having read no data. * @throws EOFException If the end of input was encountered having partially satisfied the read * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were * read and {@code allowEndOfInput} is false. @@ -94,9 +129,10 @@ public interface ExtractorInput { * @param length The number of bytes to skip from the input. * @param allowEndOfInput True if encountering the end of the input having skipped no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the skip was successful. False if the end of the input was encountered having - * skipped no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having skipped no data. * @throws EOFException If the end of input was encountered having partially satisfied the skip * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were * skipped and {@code allowEndOfInput} is false. @@ -121,12 +157,8 @@ public interface ExtractorInput { /** * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index * {@code offset}. The current read position is left unchanged. - *

    - * If the end of the input is found having peeked no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

    - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * + *

    Calling {@link #resetPeekPosition()} resets the peek position to equal the current read * position, so the caller can peek the same data again. Reading or skipping also resets the peek * position. * @@ -135,9 +167,10 @@ public interface ExtractorInput { * @param length The number of bytes to peek from the input. * @param allowEndOfInput True if encountering the end of the input having peeked no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the peek was successful. False if the end of the input was encountered having - * peeked no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having peeked no data. * @throws EOFException If the end of input was encountered having partially satisfied the peek * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were * peeked and {@code allowEndOfInput} is false. @@ -165,18 +198,16 @@ public interface ExtractorInput { void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. - *

    - * If the end of the input is encountered before advancing the peek position, then behavior is - * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is - * returned. Otherwise an {@link EOFException} is thrown. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. * * @param length The number of bytes by which to advance the peek position. * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, * and should result in {@code false} being returned. False if it should be considered an - * error, causing an {@link EOFException} to be thrown. - * @return True if advancing the peek position was successful. False if the end of the input was - * encountered before the peek position could be advanced. + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. * @throws EOFException If the end of input was encountered having partially advanced (i.e. having * advanced by at least one byte, but fewer than {@code length}), or if the end of input was * encountered before advancing and {@code allowEndOfInput} is false. @@ -187,7 +218,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,)} + * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. From 7d7c37b3248678826b3274574e863486ea1af93f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 14:28:18 +0000 Subject: [PATCH 405/424] Add NonNull annotations to metadata packages Also remove MetadataRenderer and SpliceInfoDecoder from the nullness blacklist PiperOrigin-RevId: 283744417 --- .../android/exoplayer2/metadata/Metadata.java | 18 ++++++--------- .../exoplayer2/metadata/MetadataRenderer.java | 23 +++++++++++-------- .../metadata/emsg/package-info.java | 19 +++++++++++++++ .../metadata/flac/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/icy/IcyDecoder.java | 7 +++--- .../exoplayer2/metadata/icy/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/id3/ApicFrame.java | 2 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 20 ++++++++++------ .../exoplayer2/metadata/id3/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/package-info.java | 19 +++++++++++++++ .../metadata/scte35/SpliceInfoDecoder.java | 10 +++++--- .../metadata/scte35/package-info.java | 19 +++++++++++++++ 12 files changed, 158 insertions(+), 36 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 35702da576..046c1fef55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A collection of metadata entries. @@ -57,19 +56,15 @@ public final class Metadata implements Parcelable { * @param entries The metadata entries. */ public Metadata(Entry... entries) { - this.entries = entries == null ? new Entry[0] : entries; + this.entries = entries; } /** * @param entries The metadata entries. */ public Metadata(List entries) { - if (entries != null) { - this.entries = new Entry[entries.size()]; - entries.toArray(this.entries); - } else { - this.entries = new Entry[0]; - } + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); } /* package */ Metadata(Parcel in) { @@ -118,9 +113,10 @@ public final class Metadata implements Parcelable { * @return The metadata instance with the appended entries. */ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) { - @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length); - System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length); - return new Metadata(Util.castNonNullTypeArray(merged)); + if (entriesToAppend.length == 0) { + return this; + } + return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index d738a8662e..5b287b0414 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; @@ -22,7 +24,6 @@ import android.os.Message; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; @@ -30,6 +31,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A renderer for metadata. @@ -46,12 +48,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataOutput output; @Nullable private final Handler outputHandler; private final MetadataInputBuffer buffer; - private final Metadata[] pendingMetadata; + private final @NullableType Metadata[] pendingMetadata; private final long[] pendingMetadataTimestamps; private int pendingMetadataIndex; private int pendingMetadataCount; - private MetadataDecoder decoder; + @Nullable private MetadataDecoder decoder; private boolean inputStreamEnded; private long subsampleOffsetUs; @@ -98,7 +100,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { decoder = decoderFactory.createDecoder(formats[0]); } @@ -109,7 +111,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); FormatHolder formatHolder = getFormatHolder(); @@ -124,7 +126,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - Metadata metadata = decoder.decode(buffer); + @Nullable Metadata metadata = castNonNull(decoder).decode(buffer); if (metadata != null) { List entries = new ArrayList<>(metadata.length()); decodeWrappedMetadata(metadata, entries); @@ -139,12 +141,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } } else if (result == C.RESULT_FORMAT_READ) { - subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs; } } if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { - invokeRenderer(pendingMetadata[pendingMetadataIndex]); + Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]); + invokeRenderer(metadata); pendingMetadata[pendingMetadataIndex] = null; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataCount--; @@ -158,7 +161,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { */ private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { for (int i = 0; i < metadata.length(); i++) { - Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { MetadataDecoder wrappedMetadataDecoder = decoderFactory.createDecoder(wrappedMetadataFormat); @@ -167,7 +170,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); buffer.clear(); buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); - buffer.data.put(wrappedMetadataBytes); + castNonNull(buffer.data).put(wrappedMetadataBytes); buffer.flip(); @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); if (innerMetadata != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java new file mode 100644 index 0000000000..2b03ce8df3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.emsg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java new file mode 100644 index 0000000000..343ab232e0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.flac; + +import com.google.android.exoplayer2.util.NonNullApi; 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 13d6b485b3..3834dce583 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.icy; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; @@ -28,8 +29,6 @@ import java.util.regex.Pattern; /** Decodes ICY stream information. */ 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 String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; @@ -45,8 +44,8 @@ public final class IcyDecoder implements MetadataDecoder { @VisibleForTesting /* package */ Metadata decode(String metadata) { - String name = null; - String url = null; + @Nullable String name = null; + @Nullable String url = null; int index = 0; Matcher matcher = METADATA_ELEMENT.matcher(metadata); while (matcher.find(index)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java new file mode 100644 index 0000000000..2a2d0c7fc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.icy; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index d4bedc63cc..3f4a400677 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -47,7 +47,7 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); mimeType = castNonNull(in.readString()); - description = castNonNull(in.readString()); + description = in.readString(); pictureType = in.readInt(); pictureData = castNonNull(in.createByteArray()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index ba0968cbd4..faab7f0775 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -155,7 +155,8 @@ public final class Id3Decoder implements MetadataDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. */ - private static @Nullable Id3Header decodeHeader(ParsableByteArray data) { + @Nullable + private static Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); return null; @@ -269,7 +270,8 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable Id3Frame decodeFrame( + @Nullable + private static Id3Frame decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, @@ -404,8 +406,9 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable TextInformationFrame decodeTxxxFrame( - ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + @Nullable + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -427,7 +430,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame("TXXX", description, value); } - private static @Nullable TextInformationFrame decodeTextInformationFrame( + @Nullable + private static TextInformationFrame decodeTextInformationFrame( ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -446,7 +450,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -557,7 +562,8 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { // Frame is malformed. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 0000000000..8422071842 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.id3; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java new file mode 100644 index 0000000000..a55cc1b6b3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 1153f918fc..0e161d9c69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes splice info sections and produces splice commands. @@ -37,7 +40,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); @@ -47,6 +50,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { @@ -54,7 +59,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); } - ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); sectionData.reset(data, size); @@ -68,7 +72,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader.skipBits(20); int spliceCommandLength = sectionHeader.readBits(12); int spliceCommandType = sectionHeader.readBits(8); - SpliceCommand command = null; + @Nullable SpliceCommand command = null; // Go to the start of the command by skipping all fields up to command_type. sectionData.skipBytes(14); switch (spliceCommandType) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java new file mode 100644 index 0000000000..0c4448f4d3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.scte35; + +import com.google.android.exoplayer2.util.NonNullApi; From e97b8347eb190f12191c5b97d1c086326387f9bb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 4 Dec 2019 15:41:41 +0000 Subject: [PATCH 406/424] Add IntDefs for renderer capabilities. This simplifies documentation and adds compiler checks that the correct values are used. PiperOrigin-RevId: 283754163 --- .../ext/av1/Libgav1VideoRenderer.java | 8 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 2 + .../ext/flac/LibflacAudioRenderer.java | 1 + .../ext/opus/LibopusAudioRenderer.java | 1 + .../ext/vp9/LibvpxVideoRenderer.java | 8 +- library/core/build.gradle | 2 +- .../android/exoplayer2/BaseRenderer.java | 1 + .../android/exoplayer2/NoSampleRenderer.java | 4 +- .../exoplayer2/RendererCapabilities.java | 177 +++++++++++++++--- .../audio/MediaCodecAudioRenderer.java | 17 +- .../audio/SimpleDecoderAudioRenderer.java | 17 +- .../mediacodec/MediaCodecRenderer.java | 8 +- .../exoplayer2/metadata/MetadataRenderer.java | 7 +- .../android/exoplayer2/text/TextRenderer.java | 9 +- .../trackselection/DefaultTrackSelector.java | 124 ++++++------ .../trackselection/MappingTrackSelector.java | 131 +++++++------ .../android/exoplayer2/util/EventLogger.java | 14 +- .../video/MediaCodecVideoRenderer.java | 14 +- .../video/SimpleDecoderVideoRenderer.java | 6 +- .../video/spherical/CameraMotionRenderer.java | 6 +- .../audio/SimpleDecoderAudioRendererTest.java | 1 + .../DefaultTrackSelectorTest.java | 27 ++- .../MappingTrackSelectorTest.java | 11 +- .../exoplayer2/ui/TrackSelectionView.java | 3 +- .../playbacktests/gts/DashTestRunner.java | 2 +- .../exoplayer2/testutil/FakeRenderer.java | 5 +- 26 files changed, 397 insertions(+), 209 deletions(-) diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 81cfec29fd..3d10c2579b 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -133,16 +134,17 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { } @Override + @Capabilities protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) || !Gav1Library.isAvailable()) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); } @Override diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 39d1ee4094..17292cec34 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -92,6 +92,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { Assertions.checkNotNull(format.sampleMimeType); @@ -108,6 +109,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SEAMLESS; } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index d833c47d14..3e8d727476 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -51,6 +51,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!FlacLibrary.isAvailable() diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index d17b6ebb53..3592331eff 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -83,6 +83,7 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { boolean drmIsSupported = 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 c84c3b41fe..28cb35e60f 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -223,10 +224,11 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } @Override + @Capabilities protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } boolean drmIsSupported = format.drmInitData == null @@ -234,9 +236,9 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { || (format.exoMediaCryptoType == null && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (!drmIsSupported) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); } @Override diff --git a/library/core/build.gradle b/library/core/build.gradle index 3cc14326c5..6e512e4c1e 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -16,7 +16,7 @@ apply from: '../../constants.gradle' android { compileSdkVersion project.ext.compileSdkVersion - + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 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 3cdab8baf1..bf43e74c2a 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 @@ -177,6 +177,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // RendererCapabilities implementation. @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 52bf4b3d06..b0f690d3e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -185,11 +185,13 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // RendererCapabilities implementation. @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index de0d481386..95f1749f10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -15,7 +15,12 @@ */ package com.google.android.exoplayer2; +import android.annotation.SuppressLint; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines the capabilities of a {@link Renderer}. @@ -23,10 +28,22 @@ import com.google.android.exoplayer2.util.MimeTypes; public interface RendererCapabilities { /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link + * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link + * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FORMAT_HANDLED, + FORMAT_EXCEEDS_CAPABILITIES, + FORMAT_UNSUPPORTED_DRM, + FORMAT_UNSUPPORTED_SUBTYPE, + FORMAT_UNSUPPORTED_TYPE + }) + @interface FormatSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */ int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. @@ -72,9 +89,15 @@ public interface RendererCapabilities { int FORMAT_UNSUPPORTED_TYPE = 0b000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. + * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS}, + * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}) + @interface AdaptiveSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. @@ -91,9 +114,15 @@ public interface RendererCapabilities { int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link + * #TUNNELING_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) + @interface TunnelingSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. @@ -104,6 +133,110 @@ public interface RendererCapabilities { */ int TUNNELING_NOT_SUPPORTED = 0b000000; + /** + * Combined renderer capabilities. + * + *

    This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. + * + *

    Possible values: + * + *

      + *
    • {@link FormatSupport}: The level of support for the format itself. One of {@link + * #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + *
    • {@link AdaptiveSupport}: The level of support for adapting from the format to another + * format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link + * #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
    • {@link TunnelingSupport}: The level of support for tunneling. One of {@link + * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
    + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + // Intentionally empty to prevent assignment or comparison with individual flags without masking. + @IntDef({}) + @interface Capabilities {} + + /** + * Returns {@link Capabilities} for the given {@link FormatSupport}. + * + *

    The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + * + * @param formatSupport The {@link FormatSupport}. + * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link + * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + @Capabilities + static int create(@FormatSupport int formatSupport) { + return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport} + * and {@link TunnelingSupport}. + * + * @param formatSupport The {@link FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @return The combined {@link Capabilities}. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @Capabilities + static int create( + @FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; + } + + /** + * Returns the {@link FormatSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link FormatSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @FormatSupport + static int getFormatSupport(@Capabilities int supportFlags) { + return supportFlags & FORMAT_SUPPORT_MASK; + } + + /** + * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AdaptiveSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @AdaptiveSupport + static int getAdaptiveSupport(@Capabilities int supportFlags) { + return supportFlags & ADAPTIVE_SUPPORT_MASK; + } + + /** + * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link TunnelingSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @TunnelingSupport + static int getTunnelingSupport(@Capabilities int supportFlags) { + return supportFlags & TUNNELING_SUPPORT_MASK; + } + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a @@ -115,39 +248,23 @@ public interface RendererCapabilities { int getTrackType(); /** - * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of three properties: - *

      - *
    • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
    • - *
    • The level of support for adapting from the format to another format of the same mime type. - * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
    • - *
    • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
    • - *
    - * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and - * {@link #TUNNELING_SUPPORT_MASK} respectively. + * Returns the extent to which the {@link Renderer} supports a given format. * * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. + * @return The {@link Capabilities} for this format. * @throws ExoPlaybackException If an error occurs. */ + @Capabilities int supportsFormat(Format format) throws ExoPlaybackException; /** * Returns the extent to which the {@link Renderer} supports adapting between supported formats - * that have different mime types. + * that have different MIME types. * - * @return The extent to which the renderer supports adapting between supported formats that have - * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport} for adapting between supported formats that have different + * MIME types. * @throws ExoPlaybackException If an error occurs. */ + @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; - } 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 a6a8b03448..3e48966c54 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -358,6 +359,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -365,8 +367,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; boolean supportsFormatDrm = format.drmInitData == null @@ -376,31 +379,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (supportsFormatDrm && allowPassthrough(format.channelCount, mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding)) || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { // Assume the decoder outputs 16-bit PCM, unless the input is raw. - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 21991008cb..d5a5ffe7bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -222,26 +223,28 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override + @Capabilities public final int supportsFormat(Format format) { if (!MimeTypes.isAudio(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } - int formatSupport = supportsFormatInternal(drmSessionManager, format); + @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format); if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { - return formatSupport; + return RendererCapabilities.create(formatSupport); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link - * #supportsFormat(Format)}. + * Returns the {@link FormatSupport} for the given {@link Format}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has an audio {@link Format#sampleMimeType}. - * @return The extent to which the renderer supports the format itself. + * @return The {@link FormatSupport} for this {@link Format}. */ + @FormatSupport protected abstract int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format); 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 1361bb6ad4..e8501dad75 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 @@ -452,11 +452,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override + @Capabilities public final int supportsFormat(Format format) throws ExoPlaybackException { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); @@ -466,15 +468,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns the extent to which the renderer is capable of supporting a given {@link Format}. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param mediaCodecSelector The decoder selector. * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The {@link Format}. - * @return The extent to which the renderer is capable of supporting the given format. See {@link - * #supportsFormat(Format)} for more detail. + * @return The {@link Capabilities} for this {@link Format}. * @throws DecoderQueryException If there was an error querying decoders. */ + @Capabilities protected abstract int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 5b287b0414..7a5235a466 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -91,11 +92,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 1622d68d99..35e60dcf82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -118,13 +119,15 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else if (MimeTypes.isText(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } 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 437546559c..9982ce5369 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 @@ -30,6 +30,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -1608,8 +1611,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1678,18 +1681,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * generated by this method will be overridden to account for these properties. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection.@NullableType Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1793,10 +1796,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a video renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was @@ -1806,8 +1809,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected TrackSelection.Definition selectVideoTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -1827,8 +1830,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectAdaptiveVideoTrack( TrackGroupArray groups, - int[][] formatSupport, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupport, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params) { int requiredAdaptiveSupport = params.allowVideoNonSeamlessAdaptiveness @@ -1861,7 +1864,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, @@ -1926,7 +1929,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1954,7 +1957,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1981,7 +1984,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveVideoTrack( Format format, @Nullable String mimeType, - int formatSupport, + @Capabilities int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, @@ -1998,7 +2001,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectFixedVideoTrack( - TrackGroupArray groups, int[][] formatSupports, Parameters params) { + TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -2008,7 +2011,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2071,10 +2074,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for an audio renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or @@ -2085,8 +2088,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectAudioTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -2095,7 +2098,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2148,7 +2151,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveAudioTracks( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, @@ -2202,7 +2205,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveAudioTrackCount( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2226,7 +2229,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveAudioTrack( Format format, - int formatSupport, + @Capabilities int formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2252,8 +2255,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a text renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @param selectedAudioLanguage The language of the selected audio track. May be null if the * selected text track declares no language or no text track was selected. @@ -2264,7 +2267,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectTextTrack( TrackGroupArray groups, - int[][] formatSupport, + @Capabilities int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { @@ -2273,7 +2276,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2305,22 +2308,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param trackType The type of the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable protected TrackSelection.Definition selectOtherTrack( - int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) + int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2351,6 +2354,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderers if so. * * @param mappedTrackInfo Mapped track information. + * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. * @param trackSelections The renderer track selections. @@ -2359,7 +2364,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - int[][][] renderererFormatSupports, + @Capabilities int[][][] renderererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -2408,21 +2413,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns whether a renderer supports tunneling for a {@link TrackSelection}. * - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each track, - * indexed by group index and track index (in that order). + * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track + * index (in that order). * @param trackGroups The {@link TrackGroupArray}s for the renderer. * @param selection The track selection. * @return Whether the renderer supports tunneling for the {@link TrackSelection}. */ private static boolean rendererSupportsTunneling( - int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { + @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { if (selection == null) { return false; } int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); for (int i = 0; i < selection.length(); i++) { + @Capabilities int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + if (RendererCapabilities.getTunnelingSupport(trackFormatSupport) != RendererCapabilities.TUNNELING_SUPPORTED) { return false; } @@ -2446,20 +2452,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from - * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is - * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set - * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link + * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the + * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. * - * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. - * @param allowExceedsCapabilities Whether to return true if the format support component of the - * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. - * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or - * if {@code allowExceedsCapabilities} is set and the format support component is - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @param formatSupport {@link Capabilities}. + * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if + * {@code allowExceedsCapabilities} is set and the format support is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. */ - protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { - int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + protected static boolean isSupported( + @Capabilities int formatSupport, boolean allowExceedsCapabilities) { + @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport); return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } @@ -2615,7 +2621,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int sampleRate; private final int bitrate; - public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { + public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); @@ -2754,7 +2760,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public TextTrackScore( Format format, Parameters parameters, - int trackFormatSupport, + @Capabilities int trackFormatSupport, @Nullable String selectedAudioLanguage) { isWithinRendererCapabilities = isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 425da6c1c4..9c6b2409c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -22,6 +22,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -90,25 +93,25 @@ public abstract class MappingTrackSelector extends TrackSelector { private final int rendererCount; private final int[] rendererTrackTypes; private final TrackGroupArray[] rendererTrackGroups; - private final int[] rendererMixedMimeTypeAdaptiveSupports; - private final int[][][] rendererFormatSupports; + @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports; + @Capabilities private final int[][][] rendererFormatSupports; private final TrackGroupArray unmappedTrackGroups; /** * @param rendererTrackTypes The track type handled by each renderer. * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. - * @param rendererMixedMimeTypeAdaptiveSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ @SuppressWarnings("deprecation") /* package */ MappedTrackInfo( int[] rendererTrackTypes, TrackGroupArray[] rendererTrackGroups, - int[] rendererMixedMimeTypeAdaptiveSupports, - int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports, + @Capabilities int[][][] rendererFormatSupports, TrackGroupArray unmappedTrackGroups) { this.rendererTrackTypes = rendererTrackTypes; this.rendererTrackGroups = rendererTrackGroups; @@ -149,25 +152,28 @@ public abstract class MappingTrackSelector extends TrackSelector { * Returns the extent to which a renderer can play the tracks that are mapped to it. * * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; - for (int[] trackGroupFormatSupport : rendererFormatSupport) { - for (int trackFormatSupport : trackGroupFormatSupport) { + @RendererSupport + public int getRendererSupport(int rendererIndex) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; + for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) { + for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) { int trackRendererSupport; - switch (trackFormatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) { + switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) { case RendererCapabilities.FORMAT_HANDLED: return RENDERER_SUPPORT_PLAYABLE_TRACKS; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; break; - default: + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; break; + default: + throw new IllegalStateException(); } bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } @@ -177,7 +183,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTypeSupport(int)}. */ @Deprecated - public @RendererSupport int getTrackTypeRendererSupport(int trackType) { + @RendererSupport + public int getTrackTypeRendererSupport(int trackType) { return getTypeSupport(trackType); } @@ -188,12 +195,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * returned. * * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getTypeSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @RendererSupport + public int getTypeSupport(int trackType) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; for (int i = 0; i < rendererCount; i++) { if (rendererTrackTypes[i] == trackType) { bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); @@ -204,6 +210,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ @Deprecated + @FormatSupport public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { return getTrackSupport(rendererIndex, groupIndex, trackIndex); } @@ -214,15 +221,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param rendererIndex The renderer index. * @param groupIndex The index of the track group to which the track belongs. * @param trackIndex The index of the track within the track group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link - * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + * @return The {@link FormatSupport}. */ + @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { - return rendererFormatSupports[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; + return RendererCapabilities.getFormatSupport( + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); } /** @@ -242,10 +246,9 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param groupIndex The index of the track group. * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the * renderer are included when determining support. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport( int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; @@ -253,7 +256,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] trackIndices = new int[trackCount]; int trackIndexCount = 0; for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); + @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); if (fixedSupport == RendererCapabilities.FORMAT_HANDLED || (includeCapabilitiesExceededTracks && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { @@ -270,13 +273,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; boolean multipleMimeTypes = false; String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { @@ -291,8 +293,8 @@ public abstract class MappingTrackSelector extends TrackSelector { adaptiveSupport = Math.min( adaptiveSupport, - rendererFormatSupports[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + RendererCapabilities.getAdaptiveSupport( + rendererFormatSupports[rendererIndex][groupIndex][i])); } return multipleMimeTypes ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) @@ -341,13 +343,14 @@ public abstract class MappingTrackSelector extends TrackSelector { // any renderer. int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1]; TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][]; - int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; + @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; for (int i = 0; i < rendererTrackGroups.length; i++) { rendererTrackGroups[i] = new TrackGroup[trackGroups.length]; rendererFormatSupports[i] = new int[trackGroups.length][]; } // Determine the extent to which each renderer supports mixed mimeType adaptation. + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports = getMixedMimeTypeAdaptationSupports(rendererCapabilities); @@ -358,8 +361,11 @@ public abstract class MappingTrackSelector extends TrackSelector { // Associate the group to a preferred renderer. int rendererIndex = findRenderer(rendererCapabilities, group); // Evaluate the support that the renderer provides for each track in the group. - int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length - ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group); + @Capabilities + int[] rendererFormatSupport = + rendererIndex == rendererCapabilities.length + ? new int[group.length] + : getFormatSupport(rendererCapabilities[rendererIndex], group); // Stash the results. int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex]; rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group; @@ -406,10 +412,10 @@ public abstract class MappingTrackSelector extends TrackSelector { * Given mapped track information, returns a track selection and configuration for each renderer. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupport The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return A pair consisting of the track selections and configurations for each renderer. A null * configuration indicates the renderer should be disabled, in which case the track selection * will also be null. A track selection may also be null for a non-disabled renderer if {@link @@ -419,8 +425,8 @@ public abstract class MappingTrackSelector extends TrackSelector { protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** @@ -446,12 +452,14 @@ public abstract class MappingTrackSelector extends TrackSelector { private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) - & RendererCapabilities.FORMAT_SUPPORT_MASK; + @FormatSupport + int formatSupportLevel = + RendererCapabilities.getFormatSupport( + rendererCapability.supportsFormat(group.getFormat(trackIndex))); if (formatSupportLevel > bestFormatSupportLevel) { bestRendererIndex = rendererIndex; bestFormatSupportLevel = formatSupportLevel; @@ -466,18 +474,18 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified - * {@link TrackGroup}, returning the results in an array. + * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link + * TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. * @param group The track group to evaluate. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsFormat} for each track in the group. + * @return An array containing {@link Capabilities} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. */ + @Capabilities private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group) throws ExoPlaybackException { - int[] formatSupport = new int[group.length]; + @Capabilities int[] formatSupport = new int[group.length]; for (int i = 0; i < group.length; i++) { formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i)); } @@ -489,13 +497,14 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the + * renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ + @AdaptiveSupport private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { - int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; + @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation(); } 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 a4e8e311ca..6caf549afe 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 @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -210,7 +212,8 @@ public class EventLogger implements AnalyticsListener { String adaptiveSupport = getAdaptiveSupportString( trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); @@ -552,7 +555,7 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(int formatSupport) { + private static String getFormatSupportString(@FormatSupport int formatSupport) { switch (formatSupport) { case RendererCapabilities.FORMAT_HANDLED: return "YES"; @@ -565,11 +568,12 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { + private static String getAdaptiveSupportString( + int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { return "N/A"; } @@ -581,7 +585,7 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } 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 38ac80bf26..57c3ab13fa 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 @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -360,6 +361,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -367,7 +369,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Nullable DrmInitData drmInitData = format.drmInitData; // Assume encrypted content requires secure decoders. @@ -388,7 +390,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /* requiresTunnelingDecoder= */ false); } if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } boolean supportsFormatDrm = drmInitData == null @@ -396,16 +398,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || (format.exoMediaCryptoType == null && supportsFormatDrm(drmSessionManager, drmInitData)); if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = getDecoderInfos( @@ -421,8 +424,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 73c964d1fe..9aa50e4388 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -157,6 +157,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // BaseRenderer implementation. @Override + @Capabilities public final int supportsFormat(Format format) { return supportsFormatInternal(drmSessionManager, format); } @@ -498,13 +499,14 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } /** - * Returns the extent to which the subclass supports a given format. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has a video {@link Format#sampleMimeType}. - * @return The extent to which the subclass supports the format itself. + * @return The {@link Capabilities} for this {@link Format}. * @see RendererCapabilities#supportsFormat(Format) */ + @Capabilities protected abstract int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index d1cf0abc56..35804adbe3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -48,10 +49,11 @@ public class CameraMotionRenderer extends BaseRenderer { } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType) - ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java index 6769f5049b..f8fd2fc9ca 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -58,6 +58,7 @@ public class SimpleDecoderAudioRendererTest { audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { return FORMAT_HANDLED; 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 292742b527..62d38187c4 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 @@ -1767,7 +1767,7 @@ public final class DefaultTrackSelectorTest { private static final class FakeRendererCapabilities implements RendererCapabilities { private final int trackType; - private final int supportValue; + @Capabilities private final int supportValue; /** * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all @@ -1777,19 +1777,21 @@ public final class DefaultTrackSelectorTest { * support for. */ FakeRendererCapabilities(int trackType) { - this(trackType, FORMAT_HANDLED | ADAPTIVE_SEAMLESS); + this( + trackType, + RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED)); } /** - * Returns {@link FakeRendererCapabilities} that advertises support level using given value - * for all tracks of the given type. + * Returns {@link FakeRendererCapabilities} that advertises support level using given value for + * all tracks of the given type. * * @param trackType the track type of all formats that this renderer capabilities advertises - * support for. - * @param supportValue the support level value that will be returned for formats with - * the given type. + * support for. + * @param supportValue the {@link Capabilities} that will be returned for formats with the given + * type. */ - FakeRendererCapabilities(int trackType, int supportValue) { + FakeRendererCapabilities(int trackType, @Capabilities int supportValue) { this.trackType = trackType; this.supportValue = supportValue; } @@ -1800,12 +1802,15 @@ public final class DefaultTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; + ? supportValue + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } @@ -1841,13 +1846,15 @@ public final class DefaultTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) { return format.id != null && formatToCapability.containsKey(format.id) ? formatToCapability.get(format.id) - : FORMAT_UNSUPPORTED_TYPE; + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index efb828fc57..f7bfc24881 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -23,6 +23,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -112,8 +114,8 @@ public final class MappingTrackSelectorTest { @Override protected Pair selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); lastMappedTrackInfo = mappedTrackInfo; @@ -148,12 +150,15 @@ public final class MappingTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_SEAMLESS; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index 79990e53a6..1e2d226fd6 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -371,7 +371,8 @@ public class TrackSelectionView extends LinearLayout { private boolean shouldEnableAdaptiveSelection(int groupIndex) { return allowAdaptiveSelections && trackGroups.get(groupIndex).length > 1 - && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) + && mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 8323d66614..5deed11699 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -451,7 +451,7 @@ import java.util.List; } private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + return RendererCapabilities.getFormatSupport(formatSupport) == RendererCapabilities.FORMAT_HANDLED; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index 39d3d8f7f4..987a9e33c1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -110,9 +111,11 @@ public class FakeRenderer extends BaseRenderer { } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } /** Called when the renderer reads a new format. */ From 9f44e902b14fdb1820f72ce55884efb932da0d8c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 19:03:35 +0000 Subject: [PATCH 407/424] Fix incorrect DvbParser assignment PiperOrigin-RevId: 283791815 --- .../java/com/google/android/exoplayer2/text/dvb/DvbParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3f2fef454f..0e41e4d1b6 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 @@ -645,7 +645,7 @@ import java.util.List; clutMapTable2To8 = buildClutMapTable(4, 8, data); break; case DATA_TYPE_48_TABLE_DATA: - clutMapTable2To8 = buildClutMapTable(16, 8, data); + clutMapTable4To8 = buildClutMapTable(16, 8, data); break; case DATA_TYPE_END_LINE: column = horizontalAddress; From cab05cb71de25a3af36048d91c47c61f9c2c0f17 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 20:29:56 +0000 Subject: [PATCH 408/424] Two minor nullability fixes PiperOrigin-RevId: 283810554 --- .../google/android/exoplayer2/drm/DefaultDrmSession.java | 7 ++++--- .../android/exoplayer2/extractor/MpegAudioHeader.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 0d93ec7c62..432cc6613f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -41,7 +41,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -122,8 +121,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable private RequestHandler requestHandler; @Nullable private T mediaCrypto; @Nullable private DrmSessionException lastException; - private byte @NullableType [] sessionId; - private byte @MonotonicNonNull [] offlineLicenseKeySetId; + @Nullable private byte[] sessionId; + @MonotonicNonNull private byte[] offlineLicenseKeySetId; @Nullable private KeyRequest currentKeyRequest; @Nullable private ProvisionRequest currentProvisionRequest; @@ -148,6 +147,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning * requests. */ + // the constructor does not initialize fields: sessionId + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DefaultDrmSession( UUID uuid, ExoMediaDrm mediaDrm, 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 8412b738bb..04d85b8bc5 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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; -import org.checkerframework.checker.nullness.qual.Nullable; /** * An MPEG audio frame header. From e10a78e6b7a2ccfe8d21d460b08fcfdf5fec4100 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 5 Dec 2019 13:02:01 +0000 Subject: [PATCH 409/424] Add NonNull annotations to text packages PiperOrigin-RevId: 283951181 --- .../google/android/exoplayer2/text/Cue.java | 2 +- .../text/SimpleSubtitleDecoder.java | 8 +- .../android/exoplayer2/text/TextRenderer.java | 12 +- .../exoplayer2/text/cea/Cea708Cue.java | 6 - .../exoplayer2/text/cea/Cea708Decoder.java | 4 +- .../exoplayer2/text/cea/package-info.java | 19 +++ .../exoplayer2/text/dvb/DvbParser.java | 131 +++++++++++------- .../exoplayer2/text/dvb/package-info.java | 19 +++ .../android/exoplayer2/text/package-info.java | 19 +++ .../exoplayer2/text/ssa/SsaDecoder.java | 25 ++-- .../exoplayer2/text/ssa/package-info.java | 19 +++ .../exoplayer2/text/subrip/SubripDecoder.java | 4 +- .../exoplayer2/text/ttml/package-info.java | 19 +++ .../text/webvtt/WebvttCssStyle.java | 4 +- .../exoplayer2/text/webvtt/WebvttCue.java | 4 +- .../text/webvtt/WebvttCueParser.java | 8 +- .../text/webvtt/WebvttParserUtil.java | 4 +- 17 files changed, 215 insertions(+), 92 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java 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 bd617ad626..946af76e53 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 @@ -333,7 +333,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, 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 bd561afaf8..8a1aea179a 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 @@ -18,6 +18,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 com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -29,9 +30,8 @@ public abstract class SimpleSubtitleDecoder extends private final String name; - /** - * @param name The name of the decoder. - */ + /** @param name The name of the decoder. */ + @SuppressWarnings("initialization:method.invocation.invalid") protected SimpleSubtitleDecoder(String name) { super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]); this.name = name; @@ -74,7 +74,7 @@ public abstract class SimpleSubtitleDecoder extends protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 35e60dcf82..d359eebfdb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -80,11 +80,11 @@ public final class TextRenderer extends BaseRenderer implements Callback { private boolean inputStreamEnded; private boolean outputStreamEnded; @ReplacementState private int decoderReplacementState; - private Format streamFormat; - private SubtitleDecoder decoder; - private SubtitleInputBuffer nextInputBuffer; - private SubtitleOutputBuffer subtitle; - private SubtitleOutputBuffer nextSubtitle; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; private int nextSubtitleEventIndex; /** @@ -132,7 +132,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java index fc1f0e2bdc..e04094a8dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.text.Cue; */ /* package */ final class Cea708Cue extends Cue implements Comparable { - /** - * An unset priority. - */ - public static final int PRIORITY_UNSET = -1; - /** * The priority of the cue box. */ @@ -64,5 +59,4 @@ import com.google.android.exoplayer2.text.Cue; } return 0; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index b3be88b851..4391bc0bf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -25,6 +25,7 @@ import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; @@ -152,7 +153,8 @@ public final class Cea708Decoder extends CeaDecoder { private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel, List initializationData) { + // TODO: Retrieve isWideAspectRatio from initializationData and use it. + public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java new file mode 100644 index 0000000000..cbdf178b6a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.cea; + +import com.google.android.exoplayer2.util.NonNullApi; 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 0e41e4d1b6..8382d9d9d0 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 @@ -22,6 +22,7 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses {@link Cue}s from a DVB subtitle bitstream. @@ -85,7 +87,7 @@ import java.util.List; private final ClutDefinition defaultClutDefinition; private final SubtitleService subtitleService; - private Bitmap bitmap; + @MonotonicNonNull private Bitmap bitmap; /** * Construct an instance for the given subtitle and ancillary page ids. @@ -131,7 +133,8 @@ import java.util.List; parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -147,7 +150,7 @@ import java.util.List; // Build the cues. List cues = new ArrayList<>(); - SparseArray pageRegions = subtitleService.pageComposition.regions; + SparseArray pageRegions = pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { // Save clean clipping state. canvas.save(); @@ -182,7 +185,7 @@ import java.util.List; objectData = subtitleService.ancillaryObjects.get(objectId); } if (objectData != null) { - Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, baseHorizontalAddress + regionObject.horizontalPosition, baseVerticalAddress + regionObject.verticalPosition, paint, canvas); @@ -248,7 +251,7 @@ import java.util.List; break; case SEGMENT_TYPE_PAGE_COMPOSITION: if (pageId == service.subtitlePageId) { - PageComposition current = service.pageComposition; + @Nullable PageComposition current = service.pageComposition; PageComposition pageComposition = parsePageComposition(data, dataFieldLength); if (pageComposition.state != PAGE_STATE_NORMAL) { service.pageComposition = pageComposition; @@ -261,11 +264,15 @@ import java.util.List; } break; case SEGMENT_TYPE_REGION_COMPOSITION: - PageComposition pageComposition = service.pageComposition; + @Nullable PageComposition pageComposition = service.pageComposition; if (pageId == service.subtitlePageId && pageComposition != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); if (pageComposition.state == PAGE_STATE_NORMAL) { - regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } } service.regions.put(regionComposition.id, regionComposition); } @@ -470,8 +477,8 @@ import java.util.List; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - byte[] topFieldData = null; - byte[] bottomFieldData = null; + @Nullable byte[] topFieldData = null; + @Nullable byte[] bottomFieldData = null; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); @@ -577,11 +584,15 @@ import java.util.List; // Static drawing. - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, - int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { int[] clutEntries; if (regionDepth == REGION_DEPTH_8_BIT) { clutEntries = clutDefinition.clutEntries8Bit; @@ -596,23 +607,27 @@ import java.util.List; verticalAddress + 1, paint, canvas); } - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, - int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { ParsableBitArray data = new ParsableBitArray(pixelData); int column = horizontalAddress; int line = verticalAddress; - byte[] clutMapTable2To4 = null; - byte[] clutMapTable2To8 = null; - byte[] clutMapTable4To8 = null; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; while (data.bitsLeft() != 0) { int dataType = data.readBits(8); switch (dataType) { case DATA_TYPE_2BP_CODE_STRING: - byte[] clutMapTable2ToX; + @Nullable byte[] clutMapTable2ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; } else if (regionDepth == REGION_DEPTH_4_BIT) { @@ -625,7 +640,7 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_4BP_CODE_STRING: - byte[] clutMapTable4ToX; + @Nullable byte[] clutMapTable4ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; } else { @@ -636,7 +651,9 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_8BP_CODE_STRING: - column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); break; case DATA_TYPE_24_TABLE_DATA: clutMapTable2To4 = buildClutMapTable(4, 4, data); @@ -658,11 +675,15 @@ import java.util.List; } } - /** - * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -706,11 +727,15 @@ import java.util.List; return column; } - /** - * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -760,11 +785,15 @@ import java.util.List; return column; } - /** - * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -816,18 +845,23 @@ import java.util.List; public final int subtitlePageId; public final int ancillaryPageId; - public final SparseArray regions = new SparseArray<>(); - public final SparseArray cluts = new SparseArray<>(); - public final SparseArray objects = new SparseArray<>(); - public final SparseArray ancillaryCluts = new SparseArray<>(); - public final SparseArray ancillaryObjects = new SparseArray<>(); + public final SparseArray regions; + public final SparseArray cluts; + public final SparseArray objects; + public final SparseArray ancillaryCluts; + public final SparseArray ancillaryObjects; - public DisplayDefinition displayDefinition; - public PageComposition pageComposition; + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; public SubtitleService(int subtitlePageId, int ancillaryPageId) { this.subtitlePageId = subtitlePageId; this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); } public void reset() { @@ -944,9 +978,6 @@ import java.util.List; } public void mergeFrom(RegionComposition otherRegionComposition) { - if (otherRegionComposition == null) { - return; - } SparseArray otherRegionObjects = otherRegionComposition.regionObjects; for (int i = 0; i < otherRegionObjects.size(); i++) { regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java new file mode 100644 index 0000000000..e5ec87a1a5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.dvb; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java new file mode 100644 index 0000000000..5c5b3bbc31 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text; + +import com.google.android.exoplayer2.util.NonNullApi; 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 d751772879..45d4554bb7 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,8 @@ */ package com.google.android.exoplayer2.text.ssa; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -115,7 +117,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. */ private void parseHeader(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { if ("[Script Info]".equalsIgnoreCase(currentLine)) { parseScriptInfo(data); @@ -140,7 +142,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * set to the beginning of of the first line after {@code [Script Info]}. */ private void parseScriptInfo(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { String[] infoNameAndValue = currentLine.split(":"); @@ -176,9 +178,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * at the beginning of of the first line after {@code [V4+ Styles]}. */ private static Map parseStyles(ParsableByteArray data) { - SsaStyle.Format formatInfo = null; Map styles = new LinkedHashMap<>(); - String currentLine; + @Nullable SsaStyle.Format formatInfo = null; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { @@ -188,7 +190,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); continue; } - SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); if (style != null) { styles.put(style.name, style); } @@ -205,8 +207,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + @Nullable SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { format = SsaDialogueFormat.fromFormatLine(currentLine); @@ -250,6 +253,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return; } + @Nullable SsaStyle style = styles != null && format.styleIndex != C.INDEX_UNSET ? styles.get(lineValues[format.styleIndex].trim()) @@ -281,10 +285,11 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { if (!matcher.matches()) { return C.TIME_UNSET; } - long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second. + long timestampUs = + Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second. return timestampUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java new file mode 100644 index 0000000000..cdf891d016 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ssa; + +import com.google.android.exoplayer2.util.NonNullApi; 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 20b7efe50a..0c402ac018 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 @@ -73,8 +73,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - String currentLine; + @Nullable String currentLine; while ((currentLine = subripData.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. @@ -119,7 +119,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Spanned text = Html.fromHtml(textBuilder.toString()); - String alignmentTag = null; + @Nullable String alignmentTag = null; for (int i = 0; i < tags.size(); i++) { String tag = tags.get(i); if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java new file mode 100644 index 0000000000..5b0685e24c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ttml; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 9186455702..97c0acb1ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -220,7 +220,7 @@ public final class WebvttCssStyle { return fontFamily; } - public WebvttCssStyle setFontFamily(String fontFamily) { + public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = Util.toLowerInvariant(fontFamily); return this; } @@ -264,7 +264,7 @@ public final class WebvttCssStyle { return textAlign; } - public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) { + public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index eae879c21b..bfa067e322 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -26,9 +26,7 @@ import com.google.android.exoplayer2.util.Log; import java.lang.annotation.Documented; import java.lang.annotation.Retention; -/** - * A representation of a WebVTT cue. - */ +/** A representation of a WebVTT cue. */ public final class WebvttCue extends Cue { private static final float DEFAULT_POSITION = 0.5f; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index f587d70e90..6e5bd31b4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -44,9 +44,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) - */ +/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern @@ -94,7 +92,7 @@ public final class WebvttCueParser { */ public boolean parseCue( ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { - String firstLine = webvttData.readLine(); + @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { return false; } @@ -104,7 +102,7 @@ public final class WebvttCueParser { return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); } // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); + @Nullable String secondLine = webvttData.readLine(); if (secondLine == null) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index dce8f8157f..9075083111 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -52,7 +52,7 @@ public final class WebvttParserUtil { * @param input The input from which the line should be read. */ public static boolean isWebvttHeaderLine(ParsableByteArray input) { - String line = input.readLine(); + @Nullable String line = input.readLine(); return line != null && line.startsWith(WEBVTT_HEADER); } @@ -101,7 +101,7 @@ public final class WebvttParserUtil { */ @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; + @Nullable String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. From eb5016a6ffda33a8dc7adffd10341c2f5ac9edfc Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 5 Dec 2019 14:04:43 +0000 Subject: [PATCH 410/424] Fix MCR comment line break. PiperOrigin-RevId: 283958680 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 e8501dad75..50b3ab8a0e 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 @@ -715,8 +715,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.skippedInputBufferCount += skipSource(positionUs); // We need to read any format changes despite not having a codec so that drmSession can be // updated, and so that we have the most recent format should the codec be initialized. We - // may - // also reach the end of the stream. Note that readSource will not read a sample into a + // may also reach the end of the stream. Note that readSource will not read a sample into a // flags-only buffer. readToFlagsOnlyBuffer(/* requireFormat= */ false); } From 1e609e245b6510d7cac637632ead936c5fb62dc9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 5 Dec 2019 14:59:42 +0000 Subject: [PATCH 411/424] Add format and renderer support to renderer exceptions. This makes the exception easier to interpret and helps with debugging of externally reported issues. PiperOrigin-RevId: 283965317 --- RELEASENOTES.md | 1 + .../android/exoplayer2/BaseRenderer.java | 37 ++++++++++-- .../exoplayer2/ExoPlaybackException.java | 57 +++++++++++++++++-- .../exoplayer2/ExoPlayerImplInternal.java | 16 +++++- .../exoplayer2/RendererCapabilities.java | 23 ++++++++ .../audio/MediaCodecAudioRenderer.java | 52 ++++++++++------- .../audio/SimpleDecoderAudioRenderer.java | 11 ++-- .../mediacodec/MediaCodecRenderer.java | 19 +++---- .../android/exoplayer2/text/TextRenderer.java | 4 +- .../android/exoplayer2/util/EventLogger.java | 52 +++-------------- .../google/android/exoplayer2/util/Util.java | 27 +++++++++ .../video/SimpleDecoderVideoRenderer.java | 6 +- 12 files changed, 205 insertions(+), 100 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b1b39b75b1..c9564e2d58 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,7 @@ * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s. * DRM: * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a 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 bf43e74c2a..10573af419 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 @@ -44,6 +44,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private long streamOffsetUs; private long readingPositionUs; private boolean streamIsFinal; + private boolean throwRendererExceptionIsExecuting; /** * @param trackType The track type that the renderer handles. One of the {@link C} @@ -314,8 +315,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Nullable DrmSession newSourceDrmSession = null; if (newFormat.drmInitData != null) { if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); + throw createRendererException( + new IllegalStateException("Media requires a DrmSessionManager"), newFormat); } newSourceDrmSession = drmSessionManager.acquireSession( @@ -334,6 +335,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format) { + @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; + if (format != null && !throwRendererExceptionIsExecuting) { + // Prevent recursive re-entry from subclass supportsFormat implementations. + throwRendererExceptionIsExecuting = true; + try { + formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format)); + } catch (ExoPlaybackException e) { + // Ignore, we are already failing. + } finally { + throwRendererExceptionIsExecuting = false; + } + } + return ExoPlaybackException.createForRenderer(cause, getIndex(), format, formatSupport); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -341,16 +366,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + protected final int readSource( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { 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 49aacd9638..653b6002d9 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -74,6 +75,19 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using + * at the time of the exception, or null if the renderer wasn't using a {@link Format}. + */ + @Nullable public final Format rendererFormat; + + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the + * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link + * RendererCapabilities#FORMAT_HANDLED}. + */ + @FormatSupport public final int rendererFormatSupport; + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ public final long timestampMs; @@ -86,7 +100,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForSource(IOException cause) { - return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_SOURCE, cause); } /** @@ -94,10 +108,23 @@ public final class ExoPlaybackException extends Exception { * * @param cause The cause of the failure. * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. * @return The created instance. */ - public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) { - return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex); + public static ExoPlaybackException createForRenderer( + Exception cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + return new ExoPlaybackException( + TYPE_RENDERER, + cause, + rendererIndex, + rendererFormat, + rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport); } /** @@ -107,7 +134,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForUnexpected(RuntimeException cause) { - return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_UNEXPECTED, cause); } /** @@ -127,14 +154,30 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) { - return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause); } - private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { + private ExoPlaybackException(@Type int type, Throwable cause) { + this( + type, + cause, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); + } + + private ExoPlaybackException( + @Type int type, + Throwable cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { super(cause); this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + this.rendererFormat = rendererFormat; + this.rendererFormatSupport = rendererFormatSupport; timestampMs = SystemClock.elapsedRealtime(); } @@ -142,6 +185,8 @@ public final class ExoPlaybackException extends Exception { super(message); this.type = type; rendererIndex = C.INDEX_UNSET; + rendererFormat = null; + rendererFormatSupport = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; cause = null; timestampMs = SystemClock.elapsedRealtime(); } 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 4c25c180f4..240c6436c4 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 @@ -378,7 +378,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { - Log.e(TAG, "Playback error.", e); + Log.e(TAG, getExoPlaybackExceptionMessage(e), e); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, @@ -411,6 +411,20 @@ import java.util.concurrent.atomic.AtomicBoolean; // Private methods. + private String getExoPlaybackExceptionMessage(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_RENDERER) { + return "Playback error."; + } + return "Renderer error: index=" + + e.rendererIndex + + ", type=" + + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType()) + + ", format=" + + e.rendererFormat + + ", rendererSupport=" + + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport); + } + private void setState(int state) { if (playbackInfo.playbackState != state) { playbackInfo = playbackInfo.copyWithPlaybackState(state); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 95f1749f10..a75765262b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -237,6 +237,29 @@ public interface RendererCapabilities { return supportFlags & TUNNELING_SUPPORT_MASK; } + /** + * Returns string representation of a {@link FormatSupport} flag. + * + * @param formatSupport A {@link FormatSupport} flag. + * @return A string representation of the flag. + */ + static String getFormatSupportString(@FormatSupport int formatSupport) { + switch (formatSupport) { + case RendererCapabilities.FORMAT_HANDLED: + return "YES"; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + throw new IllegalStateException(); + } + } + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a 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 3e48966c54..ae50d14728 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 @@ -90,10 +90,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsEosBufferTimestampWorkaround; private android.media.MediaFormat passthroughMediaFormat; - private @C.Encoding int pcmEncoding; - private int channelCount; - private int encoderDelay; - private int encoderPadding; + @Nullable private Format inputFormat; private long currentPositionUs; private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; @@ -551,15 +548,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { super.onInputFormatChanged(formatHolder); - Format newFormat = formatHolder.format; - eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - channelCount = newFormat.channelCount; - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + inputFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(inputFormat); } @Override @@ -575,14 +565,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = pcmEncoding; + encoding = getPcmEncoding(inputFormat); } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; - if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { - channelMap = new int[this.channelCount]; - for (int i = 0; i < this.channelCount; i++) { + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) { + channelMap = new int[inputFormat.channelCount]; + for (int i = 0; i < inputFormat.channelCount; i++) { channelMap[i] = i; } } else { @@ -590,10 +580,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, - encoderPadding); + audioSink.configure( + encoding, + channelCount, + sampleRate, + 0, + channelMap, + inputFormat.encoderDelay, + inputFormat.encoderPadding); } catch (AudioSink.ConfigurationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -820,7 +817,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } return false; } @@ -830,7 +828,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -992,6 +991,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("ms01")); } + @C.Encoding + private static int getPcmEncoding(Format format) { + // If the format is anything other than PCM then we assume that the audio decoder will output + // 16-bit PCM. + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + ? format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + private final class AudioSinkListener implements AudioSink.Listener { @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d5a5ffe7bb..5ccbf04c5c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -263,7 +263,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return; } @@ -300,7 +300,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements TraceUtil.endSection(); } catch (AudioDecoderException | AudioSink.ConfigurationException | AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -483,7 +483,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -493,7 +493,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer. + throw createRendererException(e, inputFormat); } } @@ -644,7 +645,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements codecInitializedTimestamp - codecInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } 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 50b3ab8a0e..90b1d4286e 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 @@ -463,7 +463,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } @@ -538,7 +538,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } mediaCryptoRequiresSecureDecoder = !sessionMediaCrypto.forceAllowInsecureDecoderComponents @@ -548,7 +548,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { // Wait for keys. return; @@ -559,7 +559,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -722,8 +722,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.ensureUpdated(); } catch (IllegalStateException e) { if (isMediaCodecException(e)) { - throw ExoPlaybackException.createForRenderer( - createDecoderException(e, getCodecInfo()), getIndex()); + throw createRendererException(e, inputFormat); } throw e; } @@ -1130,7 +1129,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } @@ -1186,7 +1185,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } @@ -1199,7 +1198,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -1744,7 +1743,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } setCodecDrmSession(sourceDrmSession); codecDrainState = DRAIN_STATE_NONE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index d359eebfdb..058b1c4526 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -165,7 +165,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, streamFormat); } } @@ -247,7 +247,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, streamFormat); } } 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 6caf549afe..0a303c1df7 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 @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; -import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -218,7 +217,7 @@ public class EventLogger implements AnalyticsListener { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = - getFormatSupportString( + RendererCapabilities.getFormatSupportString( mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " @@ -257,7 +256,8 @@ public class EventLogger implements AnalyticsListener { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = - getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities.getFormatSupportString( + RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); logd( " " + status @@ -289,7 +289,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType)); } @Override @@ -319,7 +319,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDecoderInitialized( EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { - logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); + logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName); } @Override @@ -327,12 +327,12 @@ public class EventLogger implements AnalyticsListener { logd( eventTime, "decoderInputFormat", - getTrackTypeString(trackType) + ", " + Format.toLogString(format)); + Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType)); } @Override @@ -555,23 +555,6 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(@FormatSupport int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: - return "NO_UNSUPPORTED_DRM"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - throw new IllegalStateException(); - } - } - private static String getAdaptiveSupportString( int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { @@ -645,27 +628,6 @@ public class EventLogger implements AnalyticsListener { } } - private static String getTrackTypeString(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_AUDIO: - return "audio"; - case C.TRACK_TYPE_DEFAULT: - return "default"; - case C.TRACK_TYPE_METADATA: - return "metadata"; - case C.TRACK_TYPE_CAMERA_MOTION: - return "camera motion"; - case C.TRACK_TYPE_NONE: - return "none"; - case C.TRACK_TYPE_TEXT: - return "text"; - case C.TRACK_TYPE_VIDEO: - return "video"; - default: - return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; - } - } - private static String getPlaybackSuppressionReasonString( @PlaybackSuppressionReason int playbackSuppressionReason) { switch (playbackSuppressionReason) { 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 23447acddf..c8a947e7d6 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 @@ -2001,6 +2001,33 @@ public final class Util { return capabilities; } + /** + * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}. + * + * @param trackType A {@code TRACK_TYPE_*} constant, + * @return A string representation of this constant. + */ + public static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_CAMERA_MOTION: + return "camera motion"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + } + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 9aa50e4388..bf0a28ffa0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -198,7 +198,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { while (feedInputBuffer()) {} TraceUtil.endSection(); } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -681,7 +681,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { decoderInitializedTimestamp - decoderInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -887,7 +887,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } From 4f363b1492345cc0ce00cb0d50ff0041f3b2c737 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 5 Dec 2019 18:19:13 +0000 Subject: [PATCH 412/424] Fix mdta handling on I/O error An I/O error could occur while handling the start of an mdta box, in which case retrying would cause another ContainerAtom to be added. Fix this by skipping the mdta header before updating container atoms. PiperOrigin-RevId: 284000715 --- .../android/exoplayer2/extractor/mp4/Mp4Extractor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 16f5b1fb29..ad58e832aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -304,13 +304,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { - if (atomType == Atom.TYPE_meta) { - maybeSkipRemainingMetaAtomHeaderBytes(input); - } // Start reading the first child atom. enterReadingAtomHeaderState(); } From 5973b76481392f5f84fedb1603ad7440f2240bd2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 5 Dec 2019 18:20:07 +0000 Subject: [PATCH 413/424] MatroskaExtractor naming cleanup - Change sampleHasReferenceBlock to a block reading variable, which is what it is (the distinction didn't matter previously, but will do so when we add lacing support in full blocks because there wont be a 1:1 relationship any more. - Move sampleRead to be a reading state variable. - Stop abbreviating "additional" Issue: #3026 PiperOrigin-RevId: 284000937 --- .../extractor/mkv/MatroskaExtractor.java | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 69bdb2cd46..ff64357ca7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -224,7 +224,7 @@ public class MatroskaExtractor implements Extractor { * BlockAddID value for ITU T.35 metadata in a VP9 track. See also * https://www.webmproject.org/docs/container/. */ - private static final int BLOCK_ADD_ID_VP9_ITU_T_35 = 4; + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; @@ -332,7 +332,7 @@ public class MatroskaExtractor implements Extractor { private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; - private final ParsableByteArray blockAddData; + private final ParsableByteArray blockAdditionalData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -360,6 +360,9 @@ public class MatroskaExtractor implements Extractor { private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; + // Reading state. + private boolean haveOutputSample; + // Block reading state. private int blockState; private long blockTimeUs; @@ -371,20 +374,19 @@ public class MatroskaExtractor implements Extractor { private int blockTrackNumberLength; @C.BufferFlags private int blockFlags; - private int blockAddId; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; // Sample reading state. private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; private boolean sampleEncodingHandled; private boolean sampleSignalByteRead; - private boolean sampleInitializationVectorRead; private boolean samplePartitionCountRead; - private byte sampleSignalByte; private int samplePartitionCount; - private int sampleCurrentNalBytesRemaining; - private int sampleBytesWritten; - private boolean sampleRead; - private boolean sampleSeenReferenceBlock; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -412,7 +414,7 @@ public class MatroskaExtractor implements Extractor { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); - blockAddData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); } @Override @@ -446,9 +448,9 @@ public class MatroskaExtractor implements Extractor { @Override public final int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - sampleRead = false; + haveOutputSample = false; boolean continueReading = true; - while (continueReading && !sampleRead) { + while (continueReading && !haveOutputSample) { continueReading = reader.read(input); if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { return Extractor.RESULT_SEEK; @@ -623,7 +625,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_GROUP: - sampleSeenReferenceBlock = false; + blockHasReferenceBlock = false; break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. @@ -681,7 +683,7 @@ public class MatroskaExtractor implements Extractor { return; } // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!sampleSeenReferenceBlock) { + if (!blockHasReferenceBlock) { blockFlags |= C.BUFFER_FLAG_KEY_FRAME; } commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); @@ -793,7 +795,7 @@ public class MatroskaExtractor implements Extractor { currentTrack.audioBitDepth = (int) value; break; case ID_REFERENCE_BLOCK: - sampleSeenReferenceBlock = true; + blockHasReferenceBlock = true; break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. @@ -935,7 +937,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_ADD_ID: - blockAddId = (int) value; + blockAdditionalId = (int) value; break; default: break; @@ -1199,7 +1201,8 @@ public class MatroskaExtractor implements Extractor { if (blockState != BLOCK_STATE_DATA) { return; } - handleBlockAdditionalData(tracks.get(blockTrackNumber), blockAddId, input, contentSize); + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); break; default: throw new ParserException("Unexpected id: " + id); @@ -1207,11 +1210,12 @@ public class MatroskaExtractor implements Extractor { } protected void handleBlockAdditionalData( - Track track, int blockAddId, ExtractorInput input, int contentSize) + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) throws IOException, InterruptedException { - if (blockAddId == BLOCK_ADD_ID_VP9_ITU_T_35 && CODEC_ID_VP9.equals(track.codecId)) { - blockAddData.reset(contentSize); - input.readFully(blockAddData.data, 0, contentSize); + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); } else { // Unhandled block additional data. input.skipFully(contentSize); @@ -1236,13 +1240,13 @@ public class MatroskaExtractor implements Extractor { if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { // Append supplemental data. - int size = blockAddData.limit(); - track.output.sampleData(blockAddData, size); - sampleBytesWritten += size; + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + sampleBytesWritten += blockAdditionalSize; } track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } - sampleRead = true; + haveOutputSample = true; resetSample(); } @@ -1375,7 +1379,7 @@ public class MatroskaExtractor implements Extractor { if (track.maxBlockAdditionId > 0) { blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; - blockAddData.reset(); + blockAdditionalData.reset(); // If there is supplemental data, the structure of the sample data is: // sample size (4 bytes) || sample data || supplemental data scratch.reset(/* limit= */ 4); From 22f25c57bbbb63fd0e7ee1ece52e455f5e534b9f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Dec 2019 11:09:44 +0000 Subject: [PATCH 414/424] MatroskaExtractor naming cleanup II - Remove "lacing" from member variables. They're used even if there is no lacing (and the fact that lacing is the way of getting multiple samples into a block isn't important). Issue: #3026 PiperOrigin-RevId: 284152447 --- .../extractor/mkv/MatroskaExtractor.java | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ff64357ca7..31f9f32484 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -367,9 +367,9 @@ public class MatroskaExtractor implements Extractor { private int blockState; private long blockTimeUs; private long blockDurationUs; - private int blockLacingSampleIndex; - private int blockLacingSampleCount; - private int[] blockLacingSampleSizes; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; @C.BufferFlags @@ -1093,9 +1093,9 @@ public class MatroskaExtractor implements Extractor { readScratch(input, 3); int lacing = (scratch.data[2] & 0x06) >> 1; if (lacing == LACING_NONE) { - blockLacingSampleCount = 1; - blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1); - blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { if (id != ID_SIMPLE_BLOCK) { throw new ParserException("Lacing only supported in SimpleBlocks."); @@ -1103,33 +1103,32 @@ public class MatroskaExtractor implements Extractor { // Read the sample count (1 byte). readScratch(input, 4); - blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1; - blockLacingSampleSizes = - ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); if (lacing == LACING_FIXED_SIZE) { int blockLacingSampleSize = - (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; - Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); } else if (lacing == LACING_XIPH) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; int byteValue; do { readScratch(input, ++headerSize); byteValue = scratch.data[headerSize - 1] & 0xFF; - blockLacingSampleSizes[sampleIndex] += byteValue; + blockSampleSizes[sampleIndex] += byteValue; } while (byteValue == 0xFF); - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else if (lacing == LACING_EBML) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; readScratch(input, ++headerSize); if (scratch.data[headerSize - 1] == 0) { throw new ParserException("No valid varint length mask found"); @@ -1157,11 +1156,13 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("EBML lacing sample size out of range."); } int intReadValue = (int) readValue; - blockLacingSampleSizes[sampleIndex] = sampleIndex == 0 - ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue; - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else { // Lacing is always in the range 0--3. @@ -1177,23 +1178,23 @@ public class MatroskaExtractor implements Extractor { blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); blockState = BLOCK_STATE_DATA; - blockLacingSampleIndex = 0; + blockSampleIndex = 0; } if (id == ID_SIMPLE_BLOCK) { // For SimpleBlock, we have metadata for each sample here. - while (blockLacingSampleIndex < blockLacingSampleCount) { - writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); - long sampleTimeUs = blockTimeUs - + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; + while (blockSampleIndex < blockSampleCount) { + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; commitSampleToOutput(track, sampleTimeUs); - blockLacingSampleIndex++; + blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { // For Block, we send the metadata at the end of the BlockGroup element since we'll know // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockLacingSampleSizes[0]); + writeSampleData(input, track, blockSampleSizes[0]); } break; From 7e93c5c0b6435fa185df9b38e1831dad2a92ed7b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Dec 2019 11:33:10 +0000 Subject: [PATCH 415/424] Enable physical display size hacks for API level 29 For AOSP TV devices that might not pass manual verification. PiperOrigin-RevId: 284154763 --- .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c8a947e7d6..0ee52dba2b 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 @@ -1927,7 +1927,7 @@ public final class Util { * @return The physical display size, in pixels. */ public static Point getPhysicalDisplaySize(Context context, Display display) { - if (Util.SDK_INT <= 28 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { + if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { // On Android TVs it is common for the UI to be configured for a lower resolution than // SurfaceViews can output. Before API 26 the Display object does not provide a way to // identify this case, and up to and including API 28 many devices still do not correctly set From bdcdabac0142c0541d74f35361c2f34c35811398 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 6 Dec 2019 23:32:04 +0000 Subject: [PATCH 416/424] Finalize release notes --- RELEASENOTES.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c9564e2d58..83ccb1be16 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.11.0 (not yet released) ### +### 2.11.0 (2019-12-11) ### * Core library: * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and @@ -82,14 +82,14 @@ ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Reconfigure audio sink when PCM encoding changes ([#6601](https://github.com/google/ExoPlayer/issues/6601)). - * Allow `AdtsExtractor` to encounter EoF when calculating average frame size + * Allow `AdtsExtractor` to encounter EOF when calculating average frame size ([#6700](https://github.com/google/ExoPlayer/issues/6700)). * Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). * Require an end time or duration for SubRip (SRT) and SubStation Alpha (SSA/ASS) subtitles. This applies to both sidecar files & subtitles [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). - * Reconfigure audio sink when PCM encoding changes - ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. @@ -101,7 +101,7 @@ * Remove `AnalyticsCollector.Factory`. Instances should be created directly, and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and - analytics reporting (TODO: link to developer guide page/blog post). + analytics reporting. * DataSource * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for HTTP and HTTPS. @@ -130,30 +130,27 @@ `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. * Use `VideoDecoderRenderer` as an implementation of `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. -* Flac extension: - * Update to use NDK r20. - * Fix build - ([#6601](https://github.com/google/ExoPlayer/issues/6601). +* Flac extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. * FFmpeg extension: * Update to use NDK r20. * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part of the extension after this change, following the instructions in the extension's readme. -* Opus extension: Update to use NDK r20. -* MediaSession extension: Make media session connector dispatch - `ACTION_SET_CAPTIONING_ENABLED`. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. * GVR extension: This extension is now deprecated. -* Demo apps (TODO: update links to point to r2.11.0 tag): - * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) +* Demo apps: + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) to show how to use the Android 10 `SurfaceControl` API with ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). * Add support for subtitle files to the - [Main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main) + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) ([#5523](https://github.com/google/ExoPlayer/issues/5523)). * Remove the IMA demo app. IMA functionality is demonstrated by the - [main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main). + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). * Add basic DRM support to the - [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). * IMA extension: Remove `AdsManager` listeners on release to avoid leaking an From 567f2a6575a9c4e92762be9d411fbe49b902ac80 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 09:56:50 +0000 Subject: [PATCH 417/424] Fix Javadoc issues PiperOrigin-RevId: 284509437 --- .../google/android/exoplayer2/extractor/ExtractorInput.java | 6 +++--- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 1b492e38c7..461b059bad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -33,7 +33,7 @@ import java.io.InputStream; * wants to read an entire block/frame/header of known length. * * - *

    {@link InputStream}-like methods

    + *

    {@link InputStream}-like methods

    * *

    The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level * access operations. The {@code length} parameter is a maximum, and each method returns the number @@ -41,7 +41,7 @@ import java.io.InputStream; * was reached, or the method was interrupted, or the operation was aborted early for another * reason. * - *

    Block-based methods

    + *

    Block-based methods

    * *

    The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user * wants to read an entire block/frame/header of known length. @@ -218,7 +218,7 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,)} + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. 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 45d4554bb7..917ac8e36e 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 @@ -72,7 +72,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * Constructs an SsaDecoder with optional format & header info. + * Constructs an SsaDecoder with optional format and header info. * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA From 0065f63f480028983ad98c8b8fbce30d766d77ed Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 14:34:12 +0000 Subject: [PATCH 418/424] MatroskaExtractor: Constrain use of sample state member variables This change constrains the use of sample state member variables to writeSampleData, finishWriteSampleData and resetWriteSampleData. Using them elsewhere gets increasingly confusing when considering features like lacing in full blocks. For example sampleBytesWritten cannot be used when calling commitSampleToOutput in this case because we need to write the sample data for multiple samples before we commit any of them. Issue: #3026 PiperOrigin-RevId: 284541942 --- .../extractor/mkv/MatroskaExtractor.java | 189 ++++++++++-------- 1 file changed, 110 insertions(+), 79 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 31f9f32484..0b7b5bd053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -377,7 +377,7 @@ public class MatroskaExtractor implements Extractor { private int blockAdditionalId; private boolean blockHasReferenceBlock; - // Sample reading state. + // Sample writing state. private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; @@ -434,7 +434,7 @@ public class MatroskaExtractor implements Extractor { blockState = BLOCK_STATE_START; reader.reset(); varintReader.reset(); - resetSample(); + resetWriteSampleData(); for (int i = 0; i < tracks.size(); i++) { tracks.valueAt(i).reset(); } @@ -686,7 +686,12 @@ public class MatroskaExtractor implements Extractor { if (!blockHasReferenceBlock) { blockFlags |= C.BUFFER_FLAG_KEY_FRAME; } - commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); + commitSampleToOutput( + tracks.get(blockTrackNumber), + blockTimeUs, + blockFlags, + blockSampleSizes[0], + /* offset= */ 0); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -1184,17 +1189,17 @@ public class MatroskaExtractor implements Extractor { if (id == ID_SIMPLE_BLOCK) { // For SimpleBlock, we have metadata for each sample here. while (blockSampleIndex < blockSampleCount) { - writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); long sampleTimeUs = blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; - commitSampleToOutput(track, sampleTimeUs); + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { // For Block, we send the metadata at the end of the BlockGroup element since we'll know // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockSampleSizes[0]); + blockSampleSizes[0] = writeSampleData(input, track, blockSampleSizes[0]); } break; @@ -1223,9 +1228,10 @@ public class MatroskaExtractor implements Extractor { } } - private void commitSampleToOutput(Track track, long timeUs) { + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { if (track.trueHdSampleRechunker != null) { - track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); } else { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (durationUs == C.TIME_UNSET) { @@ -1235,33 +1241,19 @@ public class MatroskaExtractor implements Extractor { // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); - sampleBytesWritten += subtitleSample.limit(); + size += subtitleSample.limit(); } } - if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { // Append supplemental data. int blockAdditionalSize = blockAdditionalData.limit(); track.output.sampleData(blockAdditionalData, blockAdditionalSize); - sampleBytesWritten += blockAdditionalSize; + size += blockAdditionalSize; } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); } haveOutputSample = true; - resetSample(); - } - - private void resetSample() { - sampleBytesRead = 0; - sampleBytesWritten = 0; - sampleCurrentNalBytesRemaining = 0; - sampleEncodingHandled = false; - sampleSignalByteRead = false; - samplePartitionCountRead = false; - samplePartitionCount = 0; - sampleSignalByte = (byte) 0; - sampleInitializationVectorRead = false; - sampleStrippedBytes.reset(); } /** @@ -1281,14 +1273,24 @@ public class MatroskaExtractor implements Extractor { scratch.setLimit(requiredLength); } - private void writeSampleData(ExtractorInput input, Track track, int size) + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) throws IOException, InterruptedException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { writeSubtitleSampleData(input, SUBRIP_PREFIX, size); - return; + return finishWriteSampleData(); } else if (CODEC_ID_ASS.equals(track.codecId)) { writeSubtitleSampleData(input, SSA_PREFIX, size); - return; + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1413,8 +1415,9 @@ public class MatroskaExtractor implements Extractor { while (sampleBytesRead < size) { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. - readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff, - nalUnitLengthFieldLength); + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. @@ -1423,17 +1426,21 @@ public class MatroskaExtractor implements Extractor { sampleBytesWritten += 4; } else { // Write the payload of the NAL unit. - sampleCurrentNalBytesRemaining -= - readToOutput(input, output, sampleCurrentNalBytesRemaining); + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; } } } else { if (track.trueHdSampleRechunker != null) { Assertions.checkState(sampleStrippedBytes.limit() == 0); - track.trueHdSampleRechunker.startSample(input, blockFlags, size); + track.trueHdSampleRechunker.startSample(input); } while (sampleBytesRead < size) { - readToOutput(input, output, size - sampleBytesRead); + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; } } @@ -1448,6 +1455,32 @@ public class MatroskaExtractor implements Extractor { output.sampleData(vorbisNumPageSamples, 4); sampleBytesWritten += 4; } + + return finishWriteSampleData(); + } + + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; + } + + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); } private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) @@ -1515,8 +1548,9 @@ public class MatroskaExtractor implements Extractor { int seconds = (int) (timeUs / C.MICROS_PER_SECOND); timeUs -= (seconds * C.MICROS_PER_SECOND); int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, - seconds, lastValue)); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); return timeCodeData; } @@ -1524,33 +1558,30 @@ public class MatroskaExtractor implements Extractor { * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. */ - private void readToTarget(ExtractorInput input, byte[] target, int offset, int length) + private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length) throws IOException, InterruptedException { int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); if (pendingStrippedBytes > 0) { sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); } - sampleBytesRead += length; } /** * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either * {@link #sampleStrippedBytes} or data read from {@code input}. */ - private int readToOutput(ExtractorInput input, TrackOutput output, int length) + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) throws IOException, InterruptedException { - int bytesRead; + int bytesWritten; int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); if (strippedBytesLeft > 0) { - bytesRead = Math.min(length, strippedBytesLeft); - output.sampleData(sampleStrippedBytes, bytesRead); + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); } else { - bytesRead = output.sampleData(input, length, false); + bytesWritten = output.sampleData(input, length, false); } - sampleBytesRead += bytesRead; - sampleBytesWritten += bytesRead; - return bytesRead; + return bytesWritten; } /** @@ -1725,10 +1756,11 @@ public class MatroskaExtractor implements Extractor { private final byte[] syncframePrefix; private boolean foundSyncframe; - private int sampleCount; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; private int chunkSize; - private long timeUs; - private @C.BufferFlags int blockFlags; + private int chunkOffset; public TrueHdSampleRechunker() { syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; @@ -1736,47 +1768,46 @@ public class MatroskaExtractor implements Extractor { public void reset() { foundSyncframe = false; + chunkSampleCount = 0; } - public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) - throws IOException, InterruptedException { - if (!foundSyncframe) { - input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); - input.resetPeekPosition(); - if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { - return; - } - foundSyncframe = true; - sampleCount = 0; + public void startSample(ExtractorInput input) throws IOException, InterruptedException { + if (foundSyncframe) { + return; } - if (sampleCount == 0) { - // This is the first sample in the chunk, so reset the block flags and chunk size. - this.blockFlags = blockFlags; + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; chunkSize = 0; } chunkSize += size; - } - - public void sampleMetadata(Track track, long timeUs) { - if (!foundSyncframe) { - return; - } - if (sampleCount++ == 0) { - // This is the first sample in the chunk, so update the timestamp. - this.timeUs = timeUs; - } - if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { // We haven't read enough samples to output a chunk. return; } - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + outputPendingSampleMetadata(track); } public void outputPendingSampleMetadata(Track track) { - if (foundSyncframe && sampleCount > 0) { - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; } } } From 914a8df0adfa02bcdd9eb1f7e4f596e28ec205a0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 15:02:11 +0000 Subject: [PATCH 419/424] MatroskaExtractor: Support lacing in full blocks Caveats: - Block additional data is ignored if the block is laced and contains multiple samples. Note that this is not a loss of functionality (SimpleBlock cannot have block additional data, and lacing was previously completely unsupported for Block) - Subrip and ASS samples are dropped if they're in laced blocks with multiple samples (I don't think this is valid anyway) Issue: #3026 PiperOrigin-RevId: 284545197 --- RELEASENOTES.md | 2 + .../extractor/mkv/MatroskaExtractor.java | 64 ++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 83ccb1be16..19a7868727 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -117,6 +117,8 @@ * Fix issue where streams could get stuck in an infinite buffering state after a postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). * AV1 extension: * New in this release. The AV1 extension allows use of the [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 0b7b5bd053..403f6c3d41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -682,16 +682,24 @@ public class MatroskaExtractor implements Extractor { // We've skipped this block (due to incompatible track number). return; } - // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!blockHasReferenceBlock) { - blockFlags |= C.BUFFER_FLAG_KEY_FRAME; + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); } - commitSampleToOutput( - tracks.get(blockTrackNumber), - blockTimeUs, - blockFlags, - blockSampleSizes[0], - /* offset= */ 0); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -1102,10 +1110,6 @@ public class MatroskaExtractor implements Extractor { blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { - if (id != ID_SIMPLE_BLOCK) { - throw new ParserException("Lacing only supported in SimpleBlocks."); - } - // Read the sample count (1 byte). readScratch(input, 4); blockSampleCount = (scratch.data[3] & 0xFF) + 1; @@ -1187,7 +1191,8 @@ public class MatroskaExtractor implements Extractor { } if (id == ID_SIMPLE_BLOCK) { - // For SimpleBlock, we have metadata for each sample here. + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. while (blockSampleIndex < blockSampleCount) { int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); long sampleTimeUs = @@ -1197,9 +1202,16 @@ public class MatroskaExtractor implements Extractor { } blockState = BLOCK_STATE_START; } else { - // For Block, we send the metadata at the end of the BlockGroup element since we'll know - // if the sample is a keyframe or not only at that point. - blockSampleSizes[0] = writeSampleData(input, track, blockSampleSizes[0]); + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } } break; @@ -1234,7 +1246,9 @@ public class MatroskaExtractor implements Extractor { track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); } else { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { - if (durationUs == C.TIME_UNSET) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (durationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); @@ -1246,10 +1260,16 @@ public class MatroskaExtractor implements Extractor { } if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { - // Append supplemental data. - int blockAdditionalSize = blockAdditionalData.limit(); - track.output.sampleData(blockAdditionalData, blockAdditionalSize); - size += blockAdditionalSize; + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + size += blockAdditionalSize; + } } track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); } From 1de7ec2c703de7b1d657507b497f1a9c488e61da Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 12:33:47 +0000 Subject: [PATCH 420/424] Fix bug removing entries from CacheFileMetadataIndex Issue: #6621 PiperOrigin-RevId: 284743414 --- .../cache/CacheFileMetadataIndex.java | 2 +- .../cache/CacheFileMetadataIndexTest.java | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java 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 dc27dec363..e288a5258e 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 @@ -43,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final int COLUMN_INDEX_LENGTH = 1; private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; - private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?"; private static final String[] COLUMNS = new String[] { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java new file mode 100644 index 0000000000..283487f7ea --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java @@ -0,0 +1,135 @@ +/* + * 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.cache; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.database.DatabaseIOException; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.HashSet; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests {@link CacheFileMetadataIndex}. */ +@RunWith(AndroidJUnit4.class) +public class CacheFileMetadataIndexTest { + + @Test + public void initiallyEmpty() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + assertThat(index.getAll()).isEmpty(); + } + + @Test + public void insert() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(2); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(123); + assertThat(metadata.lastTouchTimestamp).isEqualTo(456); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + metadata = all.get("name3"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemove() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + index.remove("name1"); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + index.remove("name2"); + + all = index.getAll(); + assertThat(all).isEmpty(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemoveAll() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + HashSet namesToRemove = new HashSet<>(); + namesToRemove.add("name1"); + namesToRemove.add("name2"); + index.removeAll(namesToRemove); + + Map all = index.getAll(); + assertThat(all.isEmpty()).isTrue(); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndReplace() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name1", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + } + + private static CacheFileMetadataIndex newInitializedIndex() throws DatabaseIOException { + CacheFileMetadataIndex index = + new CacheFileMetadataIndex(TestUtil.getInMemoryDatabaseProvider()); + index.initialize(/* uid= */ 1234); + return index; + } +} From b8eafea176b31a3ead1697093d6c474e9fa9d692 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 10 Dec 2019 18:04:53 +0000 Subject: [PATCH 421/424] Add missing release note entry PiperOrigin-RevId: 284792946 --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 19a7868727..67a5ba083d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -423,6 +423,7 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is provided. * When no text language preference matches, only select forced text tracks whose language matches the selected audio language. * UI: From 03b02f98df13f6e67b2d9061f3502f8f2c3f25d1 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Tue, 10 Dec 2019 18:29:59 +0000 Subject: [PATCH 422/424] Fix an issue where a keyframe was not skipped. Keyframe was rendered rather than skipped when performing an exact seek to a non-zero position close to the start of the stream. PiperOrigin-RevId: 284798460 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 67a5ba083d..5add56ca08 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -75,6 +75,9 @@ * Fix Dolby Vision fallback to AVC and HEVC. * Fix early end-of-stream detection when using video tunneling, on API level 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of the + stream. * Audio: * Fix the start of audio getting truncated when transitioning to a new item in a playlist of Opus streams. 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 90b1d4286e..820f9f003e 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 @@ -365,8 +365,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - private long lastBufferInStreamPresentationTimeUs; private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -954,6 +954,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReceivedEos = false; codecReceivedBuffers = false; + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; From 6ebc9f96c817d25d6b47af106924df3751905089 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 15:05:13 +0000 Subject: [PATCH 423/424] Fix generics warning in FakeAdaptiveMediaPeriod. Remove all generic arrays from this class. FakeAdaptiveMediaPeriod.java:171: warning: [rawtypes] found raw type: ChunkSampleStream return new ChunkSampleStream[length]; ^ missing type arguments for generic class ChunkSampleStream where T is a type-variable: T extends ChunkSource declared in class ChunkSampleStream PiperOrigin-RevId: 284761750 --- .../testutil/FakeAdaptiveMediaPeriod.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 54b5baea57..26d29d71f6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -45,7 +45,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final long durationUs; private Callback callback; - private ChunkSampleStream[] sampleStreams; + private List> sampleStreams; private SequenceableLoader sequenceableLoader; public FakeAdaptiveMediaPeriod( @@ -60,7 +60,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.durationUs = durationUs; - this.sampleStreams = newSampleStreamArray(0); + this.sampleStreams = new ArrayList<>(); this.sequenceableLoader = new CompositeSequenceableLoader(new SequenceableLoader[0]); } @@ -94,8 +94,9 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); - this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + this.sampleStreams = validStreams; + this.sequenceableLoader = + new CompositeSequenceableLoader(sampleStreams.toArray(new SequenceableLoader[0])); return returnPositionUs; } @@ -165,9 +166,4 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod public void onContinueLoadingRequested(ChunkSampleStream source) { callback.onContinueLoadingRequested(this); } - - @SuppressWarnings("unchecked") - private static ChunkSampleStream[] newSampleStreamArray(int length) { - return new ChunkSampleStream[length]; - } } From a4a9cc9fd0044b6e6ebd051d0d645c3176ae3472 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 11 Dec 2019 13:43:31 +0000 Subject: [PATCH 424/424] Suppress rawtypes warning when instantiating generic array Change FakeAdaptiveMediaPeriod back to this style for consistency. PiperOrigin-RevId: 284967667 --- .../exoplayer2/source/dash/DashMediaPeriod.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 3 ++- .../testutil/FakeAdaptiveMediaPeriod.java | 15 ++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) 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 bb8226e172..88de84603e 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 @@ -818,7 +818,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* initializationData= */ null); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } 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 42ac82e553..f7940fed1b 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 @@ -277,7 +277,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return new TrackGroupArray(trackGroups); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 26d29d71f6..011270d543 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -45,7 +45,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final long durationUs; private Callback callback; - private List> sampleStreams; + private ChunkSampleStream[] sampleStreams; private SequenceableLoader sequenceableLoader; public FakeAdaptiveMediaPeriod( @@ -60,7 +60,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.durationUs = durationUs; - this.sampleStreams = new ArrayList<>(); + this.sampleStreams = newSampleStreamArray(0); this.sequenceableLoader = new CompositeSequenceableLoader(new SequenceableLoader[0]); } @@ -94,9 +94,8 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams; - this.sequenceableLoader = - new CompositeSequenceableLoader(sampleStreams.toArray(new SequenceableLoader[0])); + this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); + this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return returnPositionUs; } @@ -166,4 +165,10 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod public void onContinueLoadingRequested(ChunkSampleStream source) { callback.onContinueLoadingRequested(this); } + + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) + private static ChunkSampleStream[] newSampleStreamArray(int length) { + return new ChunkSampleStream[length]; + } }